@11agents/cli 0.1.13 → 0.1.15

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.
package/README.md CHANGED
@@ -84,6 +84,13 @@ Run the daemon in the background:
84
84
 
85
85
  Background mode writes its pid to `~/.11agents/daemon.pid` and logs to `~/.11agents/daemon.log`.
86
86
 
87
+ Inspect daemon and task execution logs:
88
+
89
+ ```bash
90
+ 11agents logs daemon --tail 200
91
+ 11agents logs task <taskId> --project <slug>
92
+ ```
93
+
87
94
  Useful daemon options:
88
95
 
89
96
  ```bash
@@ -99,10 +106,12 @@ On startup, and every 30 minutes after that, the daemon syncs project metadata a
99
106
  - Agent-local skills: `~/.11agents/<project>/agents/<agent>/skills/`
100
107
  - Cloud database snapshot: `~/.11agents/<project>/database/snapshot.json`
101
108
  - Task scratch directory: `~/.11agents/<project>/tmp/<taskId>/`
109
+ - Task execution logs: `~/.11agents/<project>/runs/<taskId>/`
110
+ - Current claimed task marker: `~/.11agents/claim_id`
102
111
 
103
- Codex runs from `~/.11agents/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk.
112
+ Codex runs from `~/.11agents/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk. Each run directory stores `prompt.md`, raw Codex JSONL in `stdout.log`, readable Codex dialogue in `transcript.log`, `stderr.log`, `last_message.md`, `completion.json`, and `meta.json`.
104
113
 
105
- The built-in Codex worker starts task executions with `codex --ask-for-approval never --yolo exec` and prefixes the task prompt with `/goal ` by default so remote runtime tasks can run without approval prompts or sandbox restrictions. To opt a daemon back into a Codex sandbox, start it with `--codex-sandbox read-only`, `--codex-sandbox workspace-write`, or `--codex-sandbox danger-full-access`.
114
+ The built-in Codex worker starts task executions with `codex --yolo exec --json --output-last-message <run>/last_message.md` and prefixes the task prompt with `/goal ` by default so remote runtime tasks can run without approval prompts or sandbox restrictions. To opt a daemon back into a Codex sandbox, start it with `--codex-sandbox read-only`, `--codex-sandbox workspace-write`, or `--codex-sandbox danger-full-access`.
106
115
 
107
116
  The built-in task runner currently supports Codex tasks. A custom handler may export:
108
117
 
package/bin/11agents.js CHANGED
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { parseArgs } from '../src/args.js'
5
5
  import { knowledgeStatus, syncKnowledge } from '../src/commands/knowledge.js'
6
+ import { showDaemonLog, showTaskLog } from '../src/commands/logs.js'
6
7
  import { runNode } from '../src/commands/node.js'
7
8
  import { pushArtifact, pushBatch, pushObservation } from '../src/commands/push.js'
8
9
  import { registerRuntime, scanRuntime, startRuntimeDaemon } from '../src/commands/runtime.js'
@@ -24,6 +25,8 @@ Usage:
24
25
  11agents daemon status
25
26
  11agents daemon stop
26
27
  11agents daemon start --handler ./worker.js # optional custom worker override
28
+ 11agents logs daemon [--tail 200]
29
+ 11agents logs task <task-id> [--project <slug>] [--tail 120]
27
30
  11agents mcp start
28
31
  11agents validate <file>
29
32
  11agents push batch <file>
@@ -116,6 +119,17 @@ async function main() {
116
119
  return
117
120
  }
118
121
 
122
+ if (command === 'logs' && subcommand === 'daemon') {
123
+ await showDaemonLog(flags)
124
+ return
125
+ }
126
+
127
+ if (command === 'logs' && subcommand === 'task') {
128
+ if (!target) throw new Error('logs task requires a task id')
129
+ await showTaskLog(target, flags)
130
+ return
131
+ }
132
+
119
133
  if (command === 'mcp' && (!subcommand || subcommand === 'start')) {
120
134
  await startMcpServer()
121
135
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@11agents/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "11agents local runtime and telemetry CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,100 @@
1
+ import { readdir, readFile } from 'node:fs/promises'
2
+ import { homedir } from 'node:os'
3
+ import path from 'node:path'
4
+ import { backgroundPaths } from '../daemon-process.js'
5
+ import { flag } from '../args.js'
6
+
7
+ function tailText(text, lines) {
8
+ const count = Math.max(1, Number(lines) || 200)
9
+ const parts = String(text || '').replace(/\n$/, '').split('\n')
10
+ return parts.slice(-count).join('\n')
11
+ }
12
+
13
+ async function readText(filePath, fallback = '') {
14
+ try {
15
+ return await readFile(filePath, 'utf8')
16
+ } catch (error) {
17
+ if (error?.code === 'ENOENT') return fallback
18
+ throw error
19
+ }
20
+ }
21
+
22
+ async function readJson(filePath, fallback = {}) {
23
+ try {
24
+ return JSON.parse(await readFile(filePath, 'utf8'))
25
+ } catch (error) {
26
+ if (error?.code === 'ENOENT') return fallback
27
+ return fallback
28
+ }
29
+ }
30
+
31
+ export async function readDaemonLog({ homeDir = homedir(), tail = 200 } = {}) {
32
+ const text = await readText(backgroundPaths(homeDir).logPath, '')
33
+ return tailText(text, tail)
34
+ }
35
+
36
+ async function findTaskRunDir({ taskId, project = '', homeDir = homedir() }) {
37
+ if (!taskId) throw new Error('task id is required')
38
+ const baseDir = path.join(homeDir, '.11agents')
39
+ if (project) return path.join(baseDir, project, 'runs', taskId)
40
+
41
+ const entries = await readdir(baseDir, { withFileTypes: true }).catch(error => {
42
+ if (error?.code === 'ENOENT') return []
43
+ throw error
44
+ })
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory()) continue
47
+ const runDir = path.join(baseDir, entry.name, 'runs', taskId)
48
+ const meta = await readJson(path.join(runDir, 'meta.json'), null)
49
+ if (meta) return runDir
50
+ }
51
+ return path.join(baseDir, 'project', 'runs', taskId)
52
+ }
53
+
54
+ export async function formatTaskLog({ taskId, project = '', homeDir = homedir(), tail = 120 } = {}) {
55
+ const runDir = await findTaskRunDir({ taskId, project, homeDir })
56
+ const meta = await readJson(path.join(runDir, 'meta.json'), null)
57
+ if (!meta) throw new Error(`task log not found: ${taskId}`)
58
+
59
+ const stdout = tailText(await readText(path.join(runDir, 'stdout.log'), ''), tail)
60
+ const stderr = tailText(await readText(path.join(runDir, 'stderr.log'), ''), tail)
61
+ const transcript = tailText(await readText(path.join(runDir, 'transcript.log'), ''), tail)
62
+ const completion = await readText(path.join(runDir, 'completion.json'), '')
63
+ return [
64
+ `task: ${meta.task_id || taskId}`,
65
+ `provider: ${meta.provider || ''}`,
66
+ `workdir: ${meta.workdir || ''}`,
67
+ `command: ${meta.command_line || ''}`,
68
+ `exit_code: ${meta.exit_code ?? ''}`,
69
+ `run_dir: ${runDir}`,
70
+ '',
71
+ 'transcript:',
72
+ transcript,
73
+ '',
74
+ 'stdout:',
75
+ stdout,
76
+ '',
77
+ 'stderr:',
78
+ stderr,
79
+ '',
80
+ 'completion:',
81
+ completion.trim(),
82
+ ].join('\n').trimEnd()
83
+ }
84
+
85
+ export async function showDaemonLog(flags = {}, deps = {}) {
86
+ const log = await readDaemonLog({ homeDir: deps.homeDir, tail: flag(flags, 'tail', '200') })
87
+ ;(deps.log || console.log)(log)
88
+ return log
89
+ }
90
+
91
+ export async function showTaskLog(taskId, flags = {}, deps = {}) {
92
+ const text = await formatTaskLog({
93
+ taskId,
94
+ project: flag(flags, 'project'),
95
+ homeDir: deps.homeDir,
96
+ tail: flag(flags, 'tail', '120'),
97
+ })
98
+ ;(deps.log || console.log)(text)
99
+ return text
100
+ }
@@ -82,6 +82,29 @@ function runtimeDeps(overrides = {}) {
82
82
  }
83
83
  }
84
84
 
85
+ function currentClaimPath(homeDir) {
86
+ return path.join(homeDir, '.11agents', 'claim_id')
87
+ }
88
+
89
+ async function writeCurrentClaim(deps, task, machineKey) {
90
+ const payload = {
91
+ task_id: String(task.id || ''),
92
+ runtime_id: String(task.runtime_id || task.runtime?.id || ''),
93
+ machine_key: String(task.runtime?.machine_key || machineKey || ''),
94
+ }
95
+ await mkdir(path.dirname(currentClaimPath(deps.homeDir)), { recursive: true })
96
+ await writeFile(currentClaimPath(deps.homeDir), JSON.stringify(payload))
97
+ return payload
98
+ }
99
+
100
+ async function readCurrentClaim(deps) {
101
+ return readJsonFile(currentClaimPath(deps.homeDir), null)
102
+ }
103
+
104
+ async function clearCurrentClaim(deps) {
105
+ await rm(currentClaimPath(deps.homeDir), { force: true })
106
+ }
107
+
85
108
  function errorMessage(error) {
86
109
  return error instanceof Error ? error.message : String(error)
87
110
  }
@@ -263,6 +286,98 @@ function normalizeTaskCompletion(task, completion) {
263
286
  return body
264
287
  }
265
288
 
289
+ function failedClaimCompletionBody(claim, comment) {
290
+ return {
291
+ task_id: String(claim?.task_id || ''),
292
+ runtime_id: String(claim?.runtime_id || ''),
293
+ machine_key: String(claim?.machine_key || ''),
294
+ comment,
295
+ memory_delta: '',
296
+ status: 'failed',
297
+ }
298
+ }
299
+
300
+ function codexTranscriptFromJsonl(text) {
301
+ const lines = []
302
+ for (const line of String(text || '').split('\n')) {
303
+ const trimmed = line.trim()
304
+ if (!trimmed) continue
305
+ try {
306
+ const event = JSON.parse(trimmed)
307
+ if (event?.type === 'item.completed') {
308
+ const item = event.item || {}
309
+ if (item.type === 'agent_message' && item.text) {
310
+ lines.push(`assistant: ${String(item.text).trim()}`)
311
+ } else if (item.type && item.text) {
312
+ lines.push(`${item.type}: ${String(item.text).trim()}`)
313
+ }
314
+ } else if (event?.type === 'turn.completed' && event.usage) {
315
+ lines.push(`usage: ${JSON.stringify(event.usage)}`)
316
+ }
317
+ } catch {
318
+ lines.push(trimmed)
319
+ }
320
+ }
321
+ return lines.join('\n')
322
+ }
323
+
324
+ function codexAssistantTextFromJsonl(text) {
325
+ const messages = []
326
+ for (const line of String(text || '').split('\n')) {
327
+ const trimmed = line.trim()
328
+ if (!trimmed) continue
329
+ try {
330
+ const event = JSON.parse(trimmed)
331
+ const item = event?.type === 'item.completed' ? event.item || {} : {}
332
+ if (item.type === 'agent_message' && item.text) messages.push(String(item.text).trim())
333
+ } catch {}
334
+ }
335
+ return messages.join('\n\n')
336
+ }
337
+
338
+ async function failPersistedCurrentClaim(flags, deps, comment) {
339
+ const claim = await readCurrentClaim(deps)
340
+ if (!claim?.task_id || !claim?.runtime_id) return false
341
+ await deps.requestJson('/api/runtime/tasks/complete', {
342
+ method: 'POST',
343
+ body: failedClaimCompletionBody(claim, comment),
344
+ config: configFromFlags(flags),
345
+ })
346
+ await clearCurrentClaim(deps)
347
+ return true
348
+ }
349
+
350
+ function installCurrentClaimExitHandlers(flags, deps) {
351
+ let shuttingDown = false
352
+ const failAndExit = async (signal, exitCode) => {
353
+ if (shuttingDown) return
354
+ shuttingDown = true
355
+ try {
356
+ await failPersistedCurrentClaim(
357
+ flags,
358
+ deps,
359
+ `Runtime task failed locally: daemon received ${signal} before the claimed task completed.`
360
+ )
361
+ } catch (error) {
362
+ deps.log(JSON.stringify({
363
+ warning: 'failed to mark current claimed task failed during daemon shutdown',
364
+ signal,
365
+ error: errorMessage(error),
366
+ }, null, 2))
367
+ } finally {
368
+ process.exit(exitCode)
369
+ }
370
+ }
371
+ const onSigterm = () => { void failAndExit('SIGTERM', 143) }
372
+ const onSigint = () => { void failAndExit('SIGINT', 130) }
373
+ process.once('SIGTERM', onSigterm)
374
+ process.once('SIGINT', onSigint)
375
+ return () => {
376
+ process.off('SIGTERM', onSigterm)
377
+ process.off('SIGINT', onSigint)
378
+ }
379
+ }
380
+
266
381
  function extractJsonObject(text) {
267
382
  const source = String(text || '').trim()
268
383
  if (!source) return null
@@ -693,6 +808,19 @@ async function appendTaskMemoryDelta({ task, completion, workdir }) {
693
808
  return { written: true, path: indexPath }
694
809
  }
695
810
 
811
+ async function writeRunFile(runDir, fileName, content) {
812
+ if (!runDir) return
813
+ await mkdir(runDir, { recursive: true })
814
+ await writeFile(path.join(runDir, fileName), content)
815
+ }
816
+
817
+ async function updateRunMeta(runDir, patch) {
818
+ if (!runDir) return
819
+ const metaPath = path.join(runDir, 'meta.json')
820
+ const current = await readJsonFile(metaPath, {})
821
+ await writeRunFile(runDir, 'meta.json', JSON.stringify({ ...current, ...patch }, null, 2))
822
+ }
823
+
696
824
  function databaseSyncSpec(task) {
697
825
  const spec = task.database || task.cloud_database || task.workspace?.database || null
698
826
  if (!spec || typeof spec !== 'object') return null
@@ -755,7 +883,9 @@ function agentEnvironment(task) {
755
883
  async function prepareRuntimeTask(task, flags, deps, config) {
756
884
  const workdir = flag(flags, 'codex-workdir') || projectDirForTask(task, flags, deps)
757
885
  const tmpDir = path.join(workdir, 'tmp', sanitizeTaskId(task.id))
886
+ const runDir = path.join(workdir, 'runs', sanitizeTaskId(task.id))
758
887
  await mkdir(tmpDir, { recursive: true })
888
+ await mkdir(runDir, { recursive: true })
759
889
 
760
890
  const database = await syncDatabaseIfNeeded({ task, workdir, config, flags, deps })
761
891
  const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
@@ -770,6 +900,7 @@ async function prepareRuntimeTask(task, flags, deps, config) {
770
900
  return {
771
901
  workdir,
772
902
  tmp_dir: tmpDir,
903
+ run_dir: runDir,
773
904
  project_slug: projectSlugForTask(task, flags),
774
905
  readonly: true,
775
906
  env,
@@ -847,11 +978,15 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
847
978
  const codexBin = flag(flags, 'codex-bin', 'codex')
848
979
  const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
849
980
  const sandbox = flag(flags, 'codex-sandbox')
981
+ const runDir = task.execution_context?.run_dir
982
+ const lastMessagePath = runDir ? path.join(runDir, 'last_message.md') : ''
983
+ const jsonLogArgs = lastMessagePath ? ['--json', '--output-last-message', lastMessagePath] : ['--json']
850
984
  const args = sandbox
851
985
  ? [
852
986
  '--ask-for-approval',
853
987
  'never',
854
988
  'exec',
989
+ ...jsonLogArgs,
855
990
  '--skip-git-repo-check',
856
991
  '--sandbox',
857
992
  sandbox,
@@ -862,6 +997,7 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
862
997
  : [
863
998
  '--yolo',
864
999
  'exec',
1000
+ ...jsonLogArgs,
865
1001
  '--skip-git-repo-check',
866
1002
  '-C',
867
1003
  workdir,
@@ -874,10 +1010,26 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
874
1010
  if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
875
1011
 
876
1012
  const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
1013
+ await writeRunFile(runDir, 'prompt.md', `/goal ${prompt}`)
1014
+ await updateRunMeta(runDir, {
1015
+ provider: 'codex',
1016
+ command: codexBin,
1017
+ args,
1018
+ command_line: commandLine,
1019
+ workdir,
1020
+ })
877
1021
  deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
878
1022
  const result = await deps.runProcess(codexBin, args, { input: `/goal ${prompt}`, cwd: workdir, env: task.execution_context?.env || process.env })
879
- const output = String(result.stdout || '').trim()
1023
+ const rawStdout = String(result.stdout || '')
1024
+ const transcript = codexTranscriptFromJsonl(rawStdout)
1025
+ const assistantText = codexAssistantTextFromJsonl(rawStdout)
1026
+ const lastMessage = lastMessagePath ? String(await readFile(lastMessagePath, 'utf8').catch(() => '')).trim() : ''
1027
+ const output = (lastMessage || assistantText || rawStdout || transcript).trim()
880
1028
  const error = String(result.stderr || '').trim()
1029
+ await writeRunFile(runDir, 'stdout.log', rawStdout)
1030
+ await writeRunFile(runDir, 'transcript.log', transcript)
1031
+ await writeRunFile(runDir, 'stderr.log', String(result.stderr || ''))
1032
+ await updateRunMeta(runDir, { exit_code: result.code })
881
1033
  if (result.code !== 0) {
882
1034
  const body = error || output || `codex exited with status ${result.code}`
883
1035
  const trustHint = body.includes('--skip-git-repo-check')
@@ -975,6 +1127,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
975
1127
  }
976
1128
 
977
1129
  deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
1130
+ await writeCurrentClaim(deps, runtimeTask, machineKey)
978
1131
  let completion = null
979
1132
  let executionContext = null
980
1133
  if (runtimeTask.workspace?.slug) {
@@ -1002,6 +1155,15 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
1002
1155
  if (!completion) {
1003
1156
  executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
1004
1157
  runtimeTask.execution_context = executionContext
1158
+ await updateRunMeta(executionContext.run_dir, {
1159
+ task_id: String(runtimeTask.id || ''),
1160
+ runtime_id: String(runtimeTask.runtime_id || ''),
1161
+ provider: runtimeTask.runtime?.provider || runtime.provider || '',
1162
+ project_slug: executionContext.project_slug,
1163
+ agent: agentNameForTask(runtimeTask),
1164
+ issue_title: runtimeTask.issue?.title || '',
1165
+ started_at: new Date().toISOString(),
1166
+ })
1005
1167
  try {
1006
1168
  completion = await runWithRuntimeHeartbeat(
1007
1169
  () => handlerModule.handleRuntimeTask(runtimeTask),
@@ -1019,6 +1181,10 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
1019
1181
  await rm(executionContext.tmp_dir, { recursive: true, force: true })
1020
1182
  }
1021
1183
  }
1184
+ if (executionContext) {
1185
+ await writeRunFile(executionContext.run_dir, 'completion.json', JSON.stringify(normalizeTaskCompletion(runtimeTask, completion), null, 2))
1186
+ await updateRunMeta(executionContext.run_dir, { ended_at: new Date().toISOString() })
1187
+ }
1022
1188
  if (executionContext) {
1023
1189
  await appendTaskMemoryDelta({ task: runtimeTask, completion, workdir: executionContext.workdir })
1024
1190
  }
@@ -1071,6 +1237,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
1071
1237
  config,
1072
1238
  })
1073
1239
  ), deps, retryState)
1240
+ await clearCurrentClaim(deps)
1074
1241
  deps.log(JSON.stringify(result, null, 2))
1075
1242
  handled += 1
1076
1243
  }
@@ -1102,33 +1269,57 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
1102
1269
 
1103
1270
  const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
1104
1271
  const retryState = createRetryState()
1105
- let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1106
- await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
1107
- await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
1108
- if (once) return
1109
-
1110
- let lastScan = Date.now()
1111
- let lastHeartbeat = Date.now()
1112
- let lastTaskPoll = Date.now()
1113
- let lastProjectRefresh = Date.now()
1114
- while (true) {
1115
- await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
1116
- const now = Date.now()
1117
- if (now - lastScan >= scanIntervalMs) {
1118
- registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1119
- lastScan = now
1120
- lastHeartbeat = now
1121
- } else if (now - lastHeartbeat >= heartbeatIntervalMs) {
1122
- await runWithDaemonRetry('heartbeat runtime', () => heartbeatRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1123
- lastHeartbeat = now
1124
- }
1125
- if (now - lastTaskPoll >= taskIntervalMs) {
1126
- await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
1127
- lastTaskPoll = now
1272
+ const uninstallExitHandlers = installCurrentClaimExitHandlers(flags, resolvedDeps)
1273
+ try {
1274
+ let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1275
+ await runWithDaemonRetry('recover current claimed task', () => failPersistedCurrentClaim(
1276
+ flags,
1277
+ resolvedDeps,
1278
+ 'Runtime task failed locally: daemon restarted with a persisted claimed task that had not completed.'
1279
+ ), resolvedDeps, retryState)
1280
+ await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
1281
+ await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
1282
+ if (once) return
1283
+
1284
+ let lastScan = Date.now()
1285
+ let lastHeartbeat = Date.now()
1286
+ let lastTaskPoll = Date.now()
1287
+ let lastProjectRefresh = Date.now()
1288
+ while (true) {
1289
+ await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
1290
+ const now = Date.now()
1291
+ if (now - lastScan >= scanIntervalMs) {
1292
+ registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1293
+ lastScan = now
1294
+ lastHeartbeat = now
1295
+ } else if (now - lastHeartbeat >= heartbeatIntervalMs) {
1296
+ await runWithDaemonRetry('heartbeat runtime', () => heartbeatRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1297
+ lastHeartbeat = now
1298
+ }
1299
+ if (now - lastTaskPoll >= taskIntervalMs) {
1300
+ await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
1301
+ lastTaskPoll = now
1302
+ }
1303
+ if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
1304
+ await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
1305
+ lastProjectRefresh = now
1306
+ }
1128
1307
  }
1129
- if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
1130
- await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
1131
- lastProjectRefresh = now
1308
+ } catch (error) {
1309
+ try {
1310
+ await failPersistedCurrentClaim(
1311
+ flags,
1312
+ resolvedDeps,
1313
+ `Runtime task failed locally: daemon exited with error before the claimed task completed: ${errorMessage(error)}`
1314
+ )
1315
+ } catch (cleanupError) {
1316
+ resolvedDeps.log(JSON.stringify({
1317
+ warning: 'failed to mark current claimed task failed during daemon error exit',
1318
+ error: errorMessage(cleanupError),
1319
+ }, null, 2))
1132
1320
  }
1321
+ throw error
1322
+ } finally {
1323
+ if (once) uninstallExitHandlers()
1133
1324
  }
1134
1325
  }