@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@11agents/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "11agents local runtime and telemetry CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
  '',
@@ -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 args = sandbox
867
- ? [
868
- '--ask-for-approval',
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 result = await deps.runProcess(codexBin, args, { input: `/goal ${prompt}`, cwd: workdir, env: task.execution_context?.env || process.env })
904
- const output = String(result.stdout || '').trim()
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', String(result.stdout || ''))
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
- let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1147
- await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
1148
- await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
1149
- if (once) return
1150
-
1151
- let lastScan = Date.now()
1152
- let lastHeartbeat = Date.now()
1153
- let lastTaskPoll = Date.now()
1154
- let lastProjectRefresh = Date.now()
1155
- while (true) {
1156
- await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
1157
- const now = Date.now()
1158
- if (now - lastScan >= scanIntervalMs) {
1159
- registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1160
- lastScan = now
1161
- lastHeartbeat = now
1162
- } else if (now - lastHeartbeat >= heartbeatIntervalMs) {
1163
- await runWithDaemonRetry('heartbeat runtime', () => heartbeatRuntime(flags, resolvedDeps), resolvedDeps, retryState)
1164
- lastHeartbeat = now
1165
- }
1166
- if (now - lastTaskPoll >= taskIntervalMs) {
1167
- await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
1168
- lastTaskPoll = now
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
- if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
1171
- await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
1172
- lastProjectRefresh = now
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