@11agents/cli 0.1.14 → 0.1.16
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 +3 -2
- package/package.json +1 -1
- package/src/commands/logs.js +4 -0
- package/src/commands/runtime.js +295 -53
- package/src/mcp.js +32 -0
package/README.md
CHANGED
|
@@ -107,10 +107,11 @@ On startup, and every 30 minutes after that, the daemon syncs project metadata a
|
|
|
107
107
|
- Cloud database snapshot: `~/.11agents/<project>/database/snapshot.json`
|
|
108
108
|
- Task scratch directory: `~/.11agents/<project>/tmp/<taskId>/`
|
|
109
109
|
- Task execution logs: `~/.11agents/<project>/runs/<taskId>/`
|
|
110
|
+
- Current claimed task marker: `~/.11agents/claim_id`
|
|
110
111
|
|
|
111
|
-
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`.
|
|
112
113
|
|
|
113
|
-
The built-in Codex worker starts task executions with `codex --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`.
|
|
114
115
|
|
|
115
116
|
The built-in task runner currently supports Codex tasks. A custom handler may export:
|
|
116
117
|
|
package/package.json
CHANGED
package/src/commands/logs.js
CHANGED
|
@@ -58,6 +58,7 @@ export async function formatTaskLog({ taskId, project = '', homeDir = homedir(),
|
|
|
58
58
|
|
|
59
59
|
const stdout = tailText(await readText(path.join(runDir, 'stdout.log'), ''), tail)
|
|
60
60
|
const stderr = tailText(await readText(path.join(runDir, 'stderr.log'), ''), tail)
|
|
61
|
+
const transcript = tailText(await readText(path.join(runDir, 'transcript.log'), ''), tail)
|
|
61
62
|
const completion = await readText(path.join(runDir, 'completion.json'), '')
|
|
62
63
|
return [
|
|
63
64
|
`task: ${meta.task_id || taskId}`,
|
|
@@ -67,6 +68,9 @@ export async function formatTaskLog({ taskId, project = '', homeDir = homedir(),
|
|
|
67
68
|
`exit_code: ${meta.exit_code ?? ''}`,
|
|
68
69
|
`run_dir: ${runDir}`,
|
|
69
70
|
'',
|
|
71
|
+
'transcript:',
|
|
72
|
+
transcript,
|
|
73
|
+
'',
|
|
70
74
|
'stdout:',
|
|
71
75
|
stdout,
|
|
72
76
|
'',
|
package/src/commands/runtime.js
CHANGED
|
@@ -74,6 +74,7 @@ function runtimeDeps(overrides = {}) {
|
|
|
74
74
|
log: overrides.log || (value => console.log(value)),
|
|
75
75
|
homeDir: overrides.homeDir || os.homedir(),
|
|
76
76
|
runCodex: overrides.runCodex || runCodex,
|
|
77
|
+
runClaude: overrides.runClaude || runClaude,
|
|
77
78
|
runProcess: overrides.runProcess || runProcess,
|
|
78
79
|
requestJson: overrides.requestJson || requestJson,
|
|
79
80
|
sleep: overrides.sleep || sleep,
|
|
@@ -82,6 +83,29 @@ function runtimeDeps(overrides = {}) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function currentClaimPath(homeDir) {
|
|
87
|
+
return path.join(homeDir, '.11agents', 'claim_id')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function writeCurrentClaim(deps, task, machineKey) {
|
|
91
|
+
const payload = {
|
|
92
|
+
task_id: String(task.id || ''),
|
|
93
|
+
runtime_id: String(task.runtime_id || task.runtime?.id || ''),
|
|
94
|
+
machine_key: String(task.runtime?.machine_key || machineKey || ''),
|
|
95
|
+
}
|
|
96
|
+
await mkdir(path.dirname(currentClaimPath(deps.homeDir)), { recursive: true })
|
|
97
|
+
await writeFile(currentClaimPath(deps.homeDir), JSON.stringify(payload))
|
|
98
|
+
return payload
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readCurrentClaim(deps) {
|
|
102
|
+
return readJsonFile(currentClaimPath(deps.homeDir), null)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function clearCurrentClaim(deps) {
|
|
106
|
+
await rm(currentClaimPath(deps.homeDir), { force: true })
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
function errorMessage(error) {
|
|
86
110
|
return error instanceof Error ? error.message : String(error)
|
|
87
111
|
}
|
|
@@ -263,6 +287,98 @@ function normalizeTaskCompletion(task, completion) {
|
|
|
263
287
|
return body
|
|
264
288
|
}
|
|
265
289
|
|
|
290
|
+
function failedClaimCompletionBody(claim, comment) {
|
|
291
|
+
return {
|
|
292
|
+
task_id: String(claim?.task_id || ''),
|
|
293
|
+
runtime_id: String(claim?.runtime_id || ''),
|
|
294
|
+
machine_key: String(claim?.machine_key || ''),
|
|
295
|
+
comment,
|
|
296
|
+
memory_delta: '',
|
|
297
|
+
status: 'failed',
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function codexTranscriptFromJsonl(text) {
|
|
302
|
+
const lines = []
|
|
303
|
+
for (const line of String(text || '').split('\n')) {
|
|
304
|
+
const trimmed = line.trim()
|
|
305
|
+
if (!trimmed) continue
|
|
306
|
+
try {
|
|
307
|
+
const event = JSON.parse(trimmed)
|
|
308
|
+
if (event?.type === 'item.completed') {
|
|
309
|
+
const item = event.item || {}
|
|
310
|
+
if (item.type === 'agent_message' && item.text) {
|
|
311
|
+
lines.push(`assistant: ${String(item.text).trim()}`)
|
|
312
|
+
} else if (item.type && item.text) {
|
|
313
|
+
lines.push(`${item.type}: ${String(item.text).trim()}`)
|
|
314
|
+
}
|
|
315
|
+
} else if (event?.type === 'turn.completed' && event.usage) {
|
|
316
|
+
lines.push(`usage: ${JSON.stringify(event.usage)}`)
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
lines.push(trimmed)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return lines.join('\n')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function codexAssistantTextFromJsonl(text) {
|
|
326
|
+
const messages = []
|
|
327
|
+
for (const line of String(text || '').split('\n')) {
|
|
328
|
+
const trimmed = line.trim()
|
|
329
|
+
if (!trimmed) continue
|
|
330
|
+
try {
|
|
331
|
+
const event = JSON.parse(trimmed)
|
|
332
|
+
const item = event?.type === 'item.completed' ? event.item || {} : {}
|
|
333
|
+
if (item.type === 'agent_message' && item.text) messages.push(String(item.text).trim())
|
|
334
|
+
} catch {}
|
|
335
|
+
}
|
|
336
|
+
return messages.join('\n\n')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function failPersistedCurrentClaim(flags, deps, comment) {
|
|
340
|
+
const claim = await readCurrentClaim(deps)
|
|
341
|
+
if (!claim?.task_id || !claim?.runtime_id) return false
|
|
342
|
+
await deps.requestJson('/api/runtime/tasks/complete', {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
body: failedClaimCompletionBody(claim, comment),
|
|
345
|
+
config: configFromFlags(flags),
|
|
346
|
+
})
|
|
347
|
+
await clearCurrentClaim(deps)
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function installCurrentClaimExitHandlers(flags, deps) {
|
|
352
|
+
let shuttingDown = false
|
|
353
|
+
const failAndExit = async (signal, exitCode) => {
|
|
354
|
+
if (shuttingDown) return
|
|
355
|
+
shuttingDown = true
|
|
356
|
+
try {
|
|
357
|
+
await failPersistedCurrentClaim(
|
|
358
|
+
flags,
|
|
359
|
+
deps,
|
|
360
|
+
`Runtime task failed locally: daemon received ${signal} before the claimed task completed.`
|
|
361
|
+
)
|
|
362
|
+
} catch (error) {
|
|
363
|
+
deps.log(JSON.stringify({
|
|
364
|
+
warning: 'failed to mark current claimed task failed during daemon shutdown',
|
|
365
|
+
signal,
|
|
366
|
+
error: errorMessage(error),
|
|
367
|
+
}, null, 2))
|
|
368
|
+
} finally {
|
|
369
|
+
process.exit(exitCode)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const onSigterm = () => { void failAndExit('SIGTERM', 143) }
|
|
373
|
+
const onSigint = () => { void failAndExit('SIGINT', 130) }
|
|
374
|
+
process.once('SIGTERM', onSigterm)
|
|
375
|
+
process.once('SIGINT', onSigint)
|
|
376
|
+
return () => {
|
|
377
|
+
process.off('SIGTERM', onSigterm)
|
|
378
|
+
process.off('SIGINT', onSigint)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
266
382
|
function extractJsonObject(text) {
|
|
267
383
|
const source = String(text || '').trim()
|
|
268
384
|
if (!source) return null
|
|
@@ -774,12 +890,17 @@ async function prepareRuntimeTask(task, flags, deps, config) {
|
|
|
774
890
|
|
|
775
891
|
const database = await syncDatabaseIfNeeded({ task, workdir, config, flags, deps })
|
|
776
892
|
const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
|
|
893
|
+
|
|
894
|
+
// Resolve and inject the project token so MCP subprocesses (e.g. `11agents mcp start`)
|
|
895
|
+
// spawned by the runtime agent can authenticate without needing credentials on disk.
|
|
896
|
+
const projectToken = await projectSyncToken(projectTokenCandidatesForTask(task, flags), flags, deps)
|
|
777
897
|
const env = {
|
|
778
898
|
...process.env,
|
|
779
899
|
...agentEnvironment(task),
|
|
780
900
|
ELEVENAGENTS_PROJECT_DIR: workdir,
|
|
781
901
|
ELEVENAGENTS_TASK_TMP: tmpDir,
|
|
782
902
|
ELEVENAGENTS_TASK_ID: String(task.id || ''),
|
|
903
|
+
...(projectToken ? { GTM_SWARM_TOKEN: projectToken } : {}),
|
|
783
904
|
}
|
|
784
905
|
|
|
785
906
|
return {
|
|
@@ -863,34 +984,24 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
863
984
|
const codexBin = flag(flags, 'codex-bin', 'codex')
|
|
864
985
|
const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
|
|
865
986
|
const sandbox = flag(flags, 'codex-sandbox')
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
'never',
|
|
870
|
-
'exec',
|
|
871
|
-
'--skip-git-repo-check',
|
|
872
|
-
'--sandbox',
|
|
873
|
-
sandbox,
|
|
874
|
-
'-C',
|
|
875
|
-
workdir,
|
|
876
|
-
'-',
|
|
877
|
-
]
|
|
878
|
-
: [
|
|
879
|
-
'--yolo',
|
|
880
|
-
'exec',
|
|
881
|
-
'--skip-git-repo-check',
|
|
882
|
-
'-C',
|
|
883
|
-
workdir,
|
|
884
|
-
'-',
|
|
885
|
-
]
|
|
987
|
+
const runDir = task.execution_context?.run_dir
|
|
988
|
+
const lastMessagePath = runDir ? path.join(runDir, 'last_message.md') : ''
|
|
989
|
+
const jsonLogArgs = lastMessagePath ? ['--json', '--output-last-message', lastMessagePath] : ['--json']
|
|
886
990
|
const model = flag(flags, 'codex-model')
|
|
887
991
|
const profile = flag(flags, 'codex-profile')
|
|
888
|
-
const execIndex = args.indexOf('exec')
|
|
889
|
-
if (model) args.splice(execIndex + 1, 0, '--model', model)
|
|
890
|
-
if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
|
|
891
992
|
|
|
993
|
+
function buildCodexArgs(withSandbox) {
|
|
994
|
+
const built = withSandbox
|
|
995
|
+
? ['--ask-for-approval', 'never', 'exec', ...jsonLogArgs, '--skip-git-repo-check', '--sandbox', withSandbox, '-C', workdir, '-']
|
|
996
|
+
: ['--yolo', 'exec', ...jsonLogArgs, '--skip-git-repo-check', '-C', workdir, '-']
|
|
997
|
+
const execIdx = built.indexOf('exec')
|
|
998
|
+
if (model) built.splice(execIdx + 1, 0, '--model', model)
|
|
999
|
+
if (profile) built.splice(execIdx + 1, 0, '--profile', profile)
|
|
1000
|
+
return built
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const args = buildCodexArgs(sandbox)
|
|
892
1004
|
const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
|
|
893
|
-
const runDir = task.execution_context?.run_dir
|
|
894
1005
|
await writeRunFile(runDir, 'prompt.md', `/goal ${prompt}`)
|
|
895
1006
|
await updateRunMeta(runDir, {
|
|
896
1007
|
provider: 'codex',
|
|
@@ -900,10 +1011,29 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
900
1011
|
workdir,
|
|
901
1012
|
})
|
|
902
1013
|
deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
|
|
903
|
-
const
|
|
904
|
-
|
|
1014
|
+
const taskEnv = task.execution_context?.env || process.env
|
|
1015
|
+
let result = await deps.runProcess(codexBin, args, { input: `/goal ${prompt}`, cwd: workdir, env: taskEnv })
|
|
1016
|
+
|
|
1017
|
+
// When --codex-sandbox is set but bwrap is unavailable in the container (missing CAP_NET_ADMIN),
|
|
1018
|
+
// fall back to --yolo exec which runs commands directly without bwrap.
|
|
1019
|
+
if (result.code !== 0 && sandbox) {
|
|
1020
|
+
const errOutput = String(result.stderr || '') + String(result.stdout || '')
|
|
1021
|
+
if (errOutput.includes('bwrap:') && (errOutput.includes('Operation not permitted') || errOutput.includes('RTM_NEWADDR'))) {
|
|
1022
|
+
const fallbackArgs = buildCodexArgs(null)
|
|
1023
|
+
const fallbackLine = [codexBin, ...fallbackArgs].map(v => JSON.stringify(String(v))).join(' ')
|
|
1024
|
+
deps.log(JSON.stringify({ warning: 'bwrap unavailable in this environment, retrying without sandbox', fallback_command: fallbackLine }))
|
|
1025
|
+
await updateRunMeta(runDir, { bwrap_fallback: true, fallback_command_line: fallbackLine })
|
|
1026
|
+
result = await deps.runProcess(codexBin, fallbackArgs, { input: `/goal ${prompt}`, cwd: workdir, env: taskEnv })
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const rawStdout = String(result.stdout || '')
|
|
1030
|
+
const transcript = codexTranscriptFromJsonl(rawStdout)
|
|
1031
|
+
const assistantText = codexAssistantTextFromJsonl(rawStdout)
|
|
1032
|
+
const lastMessage = lastMessagePath ? String(await readFile(lastMessagePath, 'utf8').catch(() => '')).trim() : ''
|
|
1033
|
+
const output = (lastMessage || assistantText || rawStdout || transcript).trim()
|
|
905
1034
|
const error = String(result.stderr || '').trim()
|
|
906
|
-
await writeRunFile(runDir, 'stdout.log',
|
|
1035
|
+
await writeRunFile(runDir, 'stdout.log', rawStdout)
|
|
1036
|
+
await writeRunFile(runDir, 'transcript.log', transcript)
|
|
907
1037
|
await writeRunFile(runDir, 'stderr.log', String(result.stderr || ''))
|
|
908
1038
|
await updateRunMeta(runDir, { exit_code: result.code })
|
|
909
1039
|
if (result.code !== 0) {
|
|
@@ -953,10 +1083,96 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
953
1083
|
}
|
|
954
1084
|
}
|
|
955
1085
|
|
|
1086
|
+
async function runClaude({ task, prompt, flags = {}, deps }) {
|
|
1087
|
+
const claudeBin = flag(flags, 'claude-bin', 'claude')
|
|
1088
|
+
const workdir = flag(flags, 'claude-workdir') || flag(flags, 'codex-workdir') || task.execution_context?.workdir || process.cwd()
|
|
1089
|
+
const model = flag(flags, 'claude-model')
|
|
1090
|
+
const runDir = task.execution_context?.run_dir
|
|
1091
|
+
|
|
1092
|
+
// --dangerously-skip-permissions bypasses bwrap sandboxing and auto-approves MCP tool calls,
|
|
1093
|
+
// which are both required for headless remote runtimes running inside containers.
|
|
1094
|
+
const args = ['--dangerously-skip-permissions', '--print']
|
|
1095
|
+
if (model) args.push('--model', model)
|
|
1096
|
+
|
|
1097
|
+
const commandLine = [claudeBin, ...args].map(a => JSON.stringify(String(a))).join(' ')
|
|
1098
|
+
await writeRunFile(runDir, 'prompt.md', prompt)
|
|
1099
|
+
await updateRunMeta(runDir, {
|
|
1100
|
+
provider: 'claude',
|
|
1101
|
+
command: claudeBin,
|
|
1102
|
+
args,
|
|
1103
|
+
command_line: commandLine,
|
|
1104
|
+
workdir,
|
|
1105
|
+
})
|
|
1106
|
+
|
|
1107
|
+
deps.log(JSON.stringify({ running: 'claude --print', command: commandLine, workdir }, null, 2))
|
|
1108
|
+
|
|
1109
|
+
const result = await deps.runProcess(claudeBin, args, {
|
|
1110
|
+
input: prompt,
|
|
1111
|
+
cwd: workdir,
|
|
1112
|
+
env: task.execution_context?.env || process.env,
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
const rawStdout = String(result.stdout || '')
|
|
1116
|
+
const output = rawStdout.trim()
|
|
1117
|
+
const error = String(result.stderr || '').trim()
|
|
1118
|
+
|
|
1119
|
+
await writeRunFile(runDir, 'stdout.log', rawStdout)
|
|
1120
|
+
await writeRunFile(runDir, 'stderr.log', String(result.stderr || ''))
|
|
1121
|
+
await updateRunMeta(runDir, { exit_code: result.code })
|
|
1122
|
+
|
|
1123
|
+
if (result.code !== 0) {
|
|
1124
|
+
return {
|
|
1125
|
+
comment: `${error || output || `claude exited with status ${result.code}`}\n\nClaude command: ${commandLine}`,
|
|
1126
|
+
status: 'failed',
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (knowledgeDeepOrganizeSpec(task)) {
|
|
1131
|
+
const structured = extractJsonObject(output)
|
|
1132
|
+
if (structured?.knowledge_snapshot || structured?.knowledgeSnapshot) {
|
|
1133
|
+
return {
|
|
1134
|
+
...structured,
|
|
1135
|
+
comment: String(structured.comment || structured.summary || output || `Claude completed task ${task.id}.`),
|
|
1136
|
+
memory_delta: String(structured.memory_delta || structured.memoryDelta || `Claude completed task ${task.id}.`),
|
|
1137
|
+
status: structured.status ? String(structured.status) : 'in_review',
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const recoveredSnapshot = buildRecoveredDeepKnowledgeSnapshot(task, [
|
|
1141
|
+
structured?.comment || structured?.summary || '',
|
|
1142
|
+
structured?.memory_delta || structured?.memoryDelta || '',
|
|
1143
|
+
output,
|
|
1144
|
+
].filter(Boolean).join('\n\n'))
|
|
1145
|
+
if (recoveredSnapshot) {
|
|
1146
|
+
return {
|
|
1147
|
+
comment: [
|
|
1148
|
+
String(structured?.comment || structured?.summary || output || `Claude completed task ${task.id}.`).trim(),
|
|
1149
|
+
'Recovered a knowledge_snapshot from the worker output because local tooling or MCP push was unavailable.',
|
|
1150
|
+
].filter(Boolean).join('\n\n'),
|
|
1151
|
+
memory_delta: [
|
|
1152
|
+
String(structured?.memory_delta || structured?.memoryDelta || '').trim(),
|
|
1153
|
+
'Recovered deep knowledge organization snapshot from worker output.',
|
|
1154
|
+
].filter(Boolean).join('\n'),
|
|
1155
|
+
status: 'in_review',
|
|
1156
|
+
knowledge_snapshot: recoveredSnapshot,
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
comment: output || `Claude completed task ${task.id}.`,
|
|
1163
|
+
memory_delta: `Claude completed task ${task.id}.`,
|
|
1164
|
+
status: 'in_review',
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
956
1168
|
function defaultTaskHandler(flags, deps) {
|
|
957
1169
|
return {
|
|
958
1170
|
async handleRuntimeTask(task) {
|
|
959
1171
|
const provider = task.runtime?.provider || ''
|
|
1172
|
+
if (provider === 'claude') {
|
|
1173
|
+
const prompt = buildCodexPrompt(task)
|
|
1174
|
+
return deps.runClaude({ task, prompt, flags, deps })
|
|
1175
|
+
}
|
|
960
1176
|
if (provider !== 'codex') {
|
|
961
1177
|
return {
|
|
962
1178
|
comment: `unsupported runtime provider: ${provider || 'unknown'}`,
|
|
@@ -1003,6 +1219,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
1003
1219
|
}
|
|
1004
1220
|
|
|
1005
1221
|
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
1222
|
+
await writeCurrentClaim(deps, runtimeTask, machineKey)
|
|
1006
1223
|
let completion = null
|
|
1007
1224
|
let executionContext = null
|
|
1008
1225
|
if (runtimeTask.workspace?.slug) {
|
|
@@ -1112,6 +1329,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
1112
1329
|
config,
|
|
1113
1330
|
})
|
|
1114
1331
|
), deps, retryState)
|
|
1332
|
+
await clearCurrentClaim(deps)
|
|
1115
1333
|
deps.log(JSON.stringify(result, null, 2))
|
|
1116
1334
|
handled += 1
|
|
1117
1335
|
}
|
|
@@ -1143,33 +1361,57 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1143
1361
|
|
|
1144
1362
|
const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
|
|
1145
1363
|
const retryState = createRetryState()
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
await
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1364
|
+
const uninstallExitHandlers = installCurrentClaimExitHandlers(flags, resolvedDeps)
|
|
1365
|
+
try {
|
|
1366
|
+
let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1367
|
+
await runWithDaemonRetry('recover current claimed task', () => failPersistedCurrentClaim(
|
|
1368
|
+
flags,
|
|
1369
|
+
resolvedDeps,
|
|
1370
|
+
'Runtime task failed locally: daemon restarted with a persisted claimed task that had not completed.'
|
|
1371
|
+
), resolvedDeps, retryState)
|
|
1372
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
1373
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1374
|
+
if (once) return
|
|
1375
|
+
|
|
1376
|
+
let lastScan = Date.now()
|
|
1377
|
+
let lastHeartbeat = Date.now()
|
|
1378
|
+
let lastTaskPoll = Date.now()
|
|
1379
|
+
let lastProjectRefresh = Date.now()
|
|
1380
|
+
while (true) {
|
|
1381
|
+
await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
|
|
1382
|
+
const now = Date.now()
|
|
1383
|
+
if (now - lastScan >= scanIntervalMs) {
|
|
1384
|
+
registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1385
|
+
lastScan = now
|
|
1386
|
+
lastHeartbeat = now
|
|
1387
|
+
} else if (now - lastHeartbeat >= heartbeatIntervalMs) {
|
|
1388
|
+
await runWithDaemonRetry('heartbeat runtime', () => heartbeatRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1389
|
+
lastHeartbeat = now
|
|
1390
|
+
}
|
|
1391
|
+
if (now - lastTaskPoll >= taskIntervalMs) {
|
|
1392
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1393
|
+
lastTaskPoll = now
|
|
1394
|
+
}
|
|
1395
|
+
if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
|
|
1396
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
1397
|
+
lastProjectRefresh = now
|
|
1398
|
+
}
|
|
1169
1399
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
try {
|
|
1402
|
+
await failPersistedCurrentClaim(
|
|
1403
|
+
flags,
|
|
1404
|
+
resolvedDeps,
|
|
1405
|
+
`Runtime task failed locally: daemon exited with error before the claimed task completed: ${errorMessage(error)}`
|
|
1406
|
+
)
|
|
1407
|
+
} catch (cleanupError) {
|
|
1408
|
+
resolvedDeps.log(JSON.stringify({
|
|
1409
|
+
warning: 'failed to mark current claimed task failed during daemon error exit',
|
|
1410
|
+
error: errorMessage(cleanupError),
|
|
1411
|
+
}, null, 2))
|
|
1173
1412
|
}
|
|
1413
|
+
throw error
|
|
1414
|
+
} finally {
|
|
1415
|
+
if (once) uninstallExitHandlers()
|
|
1174
1416
|
}
|
|
1175
1417
|
}
|
package/src/mcp.js
CHANGED
|
@@ -17,6 +17,22 @@ const TOOLS = [
|
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
name: 'database_sync',
|
|
22
|
+
description: 'Pull or push the cloud project database snapshot with a project token.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
required: ['project', 'mode'],
|
|
26
|
+
properties: {
|
|
27
|
+
project: { type: 'string' },
|
|
28
|
+
mode: { type: 'string', enum: ['pull', 'push'] },
|
|
29
|
+
token: { type: 'string' },
|
|
30
|
+
server: { type: 'string' },
|
|
31
|
+
snapshot: { type: 'object' },
|
|
32
|
+
metadata: { type: 'object' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
20
36
|
]
|
|
21
37
|
|
|
22
38
|
function textResult(value) {
|
|
@@ -64,6 +80,22 @@ export async function handleMcpRequest(request, deps = {}) {
|
|
|
64
80
|
})
|
|
65
81
|
return textResult(result)
|
|
66
82
|
}
|
|
83
|
+
if (name === 'database_sync') {
|
|
84
|
+
const token = await resolveProjectToken(args.project, {
|
|
85
|
+
homeDir: deps.homeDir,
|
|
86
|
+
token: args.token,
|
|
87
|
+
})
|
|
88
|
+
const server = args.server || 'https://app.11agents.ai'
|
|
89
|
+
const result = await (deps.requestJson || requestJson)(
|
|
90
|
+
`/api/projects/${encodeURIComponent(args.project)}/database/sync`,
|
|
91
|
+
{
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: { mode: args.mode || 'pull', snapshot: args.snapshot, metadata: args.metadata },
|
|
94
|
+
config: { token, server },
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
return textResult(result)
|
|
98
|
+
}
|
|
67
99
|
throw new Error(`unknown MCP tool: ${name}`)
|
|
68
100
|
}
|
|
69
101
|
|