@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 +11 -2
- package/bin/11agents.js +14 -0
- package/package.json +1 -1
- package/src/commands/logs.js +100 -0
- package/src/commands/runtime.js +218 -27
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 --
|
|
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
|
@@ -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
|
+
}
|
package/src/commands/runtime.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
await
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
}
|