@11agents/cli 0.1.14 → 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
@@ -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.15",
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
  '',
@@ -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
@@ -863,11 +978,15 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
863
978
  const codexBin = flag(flags, 'codex-bin', 'codex')
864
979
  const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
865
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']
866
984
  const args = sandbox
867
985
  ? [
868
986
  '--ask-for-approval',
869
987
  'never',
870
988
  'exec',
989
+ ...jsonLogArgs,
871
990
  '--skip-git-repo-check',
872
991
  '--sandbox',
873
992
  sandbox,
@@ -878,6 +997,7 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
878
997
  : [
879
998
  '--yolo',
880
999
  'exec',
1000
+ ...jsonLogArgs,
881
1001
  '--skip-git-repo-check',
882
1002
  '-C',
883
1003
  workdir,
@@ -890,7 +1010,6 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
890
1010
  if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
891
1011
 
892
1012
  const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
893
- const runDir = task.execution_context?.run_dir
894
1013
  await writeRunFile(runDir, 'prompt.md', `/goal ${prompt}`)
895
1014
  await updateRunMeta(runDir, {
896
1015
  provider: 'codex',
@@ -901,9 +1020,14 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
901
1020
  })
902
1021
  deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
903
1022
  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()
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()
905
1028
  const error = String(result.stderr || '').trim()
906
- await writeRunFile(runDir, 'stdout.log', String(result.stdout || ''))
1029
+ await writeRunFile(runDir, 'stdout.log', rawStdout)
1030
+ await writeRunFile(runDir, 'transcript.log', transcript)
907
1031
  await writeRunFile(runDir, 'stderr.log', String(result.stderr || ''))
908
1032
  await updateRunMeta(runDir, { exit_code: result.code })
909
1033
  if (result.code !== 0) {
@@ -1003,6 +1127,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
1003
1127
  }
1004
1128
 
1005
1129
  deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
1130
+ await writeCurrentClaim(deps, runtimeTask, machineKey)
1006
1131
  let completion = null
1007
1132
  let executionContext = null
1008
1133
  if (runtimeTask.workspace?.slug) {
@@ -1112,6 +1237,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
1112
1237
  config,
1113
1238
  })
1114
1239
  ), deps, retryState)
1240
+ await clearCurrentClaim(deps)
1115
1241
  deps.log(JSON.stringify(result, null, 2))
1116
1242
  handled += 1
1117
1243
  }
@@ -1143,33 +1269,57 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
1143
1269
 
1144
1270
  const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
1145
1271
  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
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
+ }
1169
1307
  }
1170
- if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
1171
- await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
1172
- 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))
1173
1320
  }
1321
+ throw error
1322
+ } finally {
1323
+ if (once) uninstallExitHandlers()
1174
1324
  }
1175
1325
  }