@11agents/cli 0.1.18 → 0.1.20
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 +19 -7
- package/bin/11agents.js +3 -3
- package/package.json +1 -1
- package/src/commands/runtime.js +189 -157
package/README.md
CHANGED
|
@@ -125,17 +125,29 @@ export async function handleRuntimeTask(task) {
|
|
|
125
125
|
}
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
## MCP
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
|
|
128
|
+
## Hosted MCP
|
|
129
|
+
|
|
130
|
+
Runtime agents and MCP clients should use the hosted project MCP endpoint:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"11agents-project-sync": {
|
|
136
|
+
"url": "https://app.11agents.ai/mcp",
|
|
137
|
+
"transport": "streamable-http",
|
|
138
|
+
"headers": {
|
|
139
|
+
"Authorization": "Bearer $PROJECT_SWARM_TOKEN"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
134
144
|
```
|
|
135
145
|
|
|
136
|
-
|
|
146
|
+
The hosted server exposes project-scoped tools such as:
|
|
137
147
|
|
|
138
148
|
- `knowledge_sync` — pull/push the project knowledge base between cloud and `~/.11agents/<project>/knowledge_base/`.
|
|
149
|
+
- `database_sync` — pull/push the cloud project database snapshot.
|
|
150
|
+
- `agent_planning` — list, import, generate, and dispatch agent calendar work.
|
|
139
151
|
|
|
140
152
|
## Telemetry Compatibility
|
|
141
153
|
|
package/bin/11agents.js
CHANGED
|
@@ -21,13 +21,12 @@ Usage:
|
|
|
21
21
|
11agents -v | --version
|
|
22
22
|
11agents runtime scan
|
|
23
23
|
11agents runtime register [--server <url>] [--token <token>] [--machine <key>]
|
|
24
|
-
11agents daemon start [--server <url>] [--token <token>] [--machine <key>] [--task-interval <seconds>] [--project-refresh-interval <seconds>] [--background]
|
|
24
|
+
11agents daemon start [--server <url>] [--token <token>] [--machine <key>] [--concurrency <n>] [--task-interval <seconds>] [--project-refresh-interval <seconds>] [--background]
|
|
25
25
|
11agents daemon status
|
|
26
26
|
11agents daemon stop
|
|
27
27
|
11agents daemon start --handler ./worker.js # optional custom worker override
|
|
28
28
|
11agents logs daemon [--tail 200]
|
|
29
29
|
11agents logs task <task-id> [--project <slug>] [--tail 120]
|
|
30
|
-
11agents mcp start
|
|
31
30
|
11agents validate <file>
|
|
32
31
|
11agents push batch <file>
|
|
33
32
|
11agents push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id>
|
|
@@ -42,7 +41,7 @@ Environment:
|
|
|
42
41
|
GTM_WRITES_TOKEN control-plane token for runtime registration
|
|
43
42
|
ELEVENAGENTS_MACHINE stable machine key, defaults to hostname
|
|
44
43
|
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
45
|
-
~/.11agents/credentials project token map for MCP
|
|
44
|
+
~/.11agents/credentials project token map for hosted MCP and sync commands
|
|
46
45
|
|
|
47
46
|
Runtime task workspace:
|
|
48
47
|
Daemon project headquarters live under ~/.11agents/<project>/.
|
|
@@ -99,6 +98,7 @@ async function main() {
|
|
|
99
98
|
console.log(`log: ${result.logPath}`)
|
|
100
99
|
return
|
|
101
100
|
}
|
|
101
|
+
if (flags.concurrency === undefined) flags.concurrency = '3'
|
|
102
102
|
await startRuntimeDaemon(flags)
|
|
103
103
|
return
|
|
104
104
|
}
|
package/package.json
CHANGED
package/src/commands/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { appendFileSync, readFileSync } from 'node:fs'
|
|
4
|
-
import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { appendFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
5
5
|
import os from 'node:os'
|
|
6
6
|
import { dirname, resolve } from 'node:path'
|
|
7
7
|
import path from 'node:path'
|
|
@@ -86,27 +86,49 @@ function runtimeDeps(overrides = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function
|
|
89
|
+
function legacyClaimPath(homeDir) {
|
|
90
90
|
return path.join(homeDir, '.11agents', 'claim_id')
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function claimsDirPath(homeDir) {
|
|
94
|
+
return path.join(homeDir, '.11agents', 'claims')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function claimFilePath(homeDir, taskId) {
|
|
98
|
+
return path.join(claimsDirPath(homeDir), `${sanitizeTaskId(taskId)}.json`)
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
async function writeCurrentClaim(deps, task, machineKey) {
|
|
94
102
|
const payload = {
|
|
95
103
|
task_id: String(task.id || ''),
|
|
96
104
|
runtime_id: String(task.runtime_id || task.runtime?.id || ''),
|
|
97
105
|
machine_key: String(task.runtime?.machine_key || machineKey || ''),
|
|
98
106
|
}
|
|
99
|
-
await mkdir(
|
|
100
|
-
await writeFile(
|
|
107
|
+
await mkdir(claimsDirPath(deps.homeDir), { recursive: true })
|
|
108
|
+
await writeFile(claimFilePath(deps.homeDir, task.id), JSON.stringify(payload))
|
|
101
109
|
return payload
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
async function
|
|
105
|
-
|
|
112
|
+
async function clearCurrentClaim(deps, taskId) {
|
|
113
|
+
await rm(claimFilePath(deps.homeDir, taskId), { force: true })
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
async function
|
|
109
|
-
|
|
116
|
+
async function readAllCurrentClaims(deps) {
|
|
117
|
+
const claims = []
|
|
118
|
+
// Backward compat: consume legacy single-claim file
|
|
119
|
+
const legacy = await readJsonFile(legacyClaimPath(deps.homeDir), null)
|
|
120
|
+
if (legacy?.task_id && legacy?.runtime_id) {
|
|
121
|
+
claims.push({ ...legacy, _path: legacyClaimPath(deps.homeDir) })
|
|
122
|
+
}
|
|
123
|
+
// Per-task claim files
|
|
124
|
+
const entries = await readdir(claimsDirPath(deps.homeDir)).catch(err => (err?.code === 'ENOENT' ? [] : Promise.reject(err)))
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!entry.endsWith('.json')) continue
|
|
127
|
+
const filePath = path.join(claimsDirPath(deps.homeDir), entry)
|
|
128
|
+
const claim = await readJsonFile(filePath, null)
|
|
129
|
+
if (claim?.task_id && claim?.runtime_id) claims.push({ ...claim, _path: filePath })
|
|
130
|
+
}
|
|
131
|
+
return claims
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
function errorMessage(error) {
|
|
@@ -339,15 +361,20 @@ function codexAssistantTextFromJsonl(text) {
|
|
|
339
361
|
return messages.join('\n\n')
|
|
340
362
|
}
|
|
341
363
|
|
|
342
|
-
async function
|
|
343
|
-
const
|
|
344
|
-
if (!
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
364
|
+
async function failAllPersistedClaims(flags, deps, comment) {
|
|
365
|
+
const claims = await readAllCurrentClaims(deps)
|
|
366
|
+
if (!claims.length) return false
|
|
367
|
+
const config = configFromFlags(flags)
|
|
368
|
+
for (const claim of claims) {
|
|
369
|
+
try {
|
|
370
|
+
await deps.requestJson('/api/runtime/tasks/complete', {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
body: failedClaimCompletionBody(claim, comment),
|
|
373
|
+
config,
|
|
374
|
+
})
|
|
375
|
+
} catch {}
|
|
376
|
+
await rm(claim._path, { force: true })
|
|
377
|
+
}
|
|
351
378
|
return true
|
|
352
379
|
}
|
|
353
380
|
|
|
@@ -357,14 +384,14 @@ function installCurrentClaimExitHandlers(flags, deps) {
|
|
|
357
384
|
if (shuttingDown) return
|
|
358
385
|
shuttingDown = true
|
|
359
386
|
try {
|
|
360
|
-
await
|
|
387
|
+
await failAllPersistedClaims(
|
|
361
388
|
flags,
|
|
362
389
|
deps,
|
|
363
390
|
`Runtime task failed locally: daemon received ${signal} before the claimed task completed.`
|
|
364
391
|
)
|
|
365
392
|
} catch (error) {
|
|
366
393
|
deps.log(JSON.stringify({
|
|
367
|
-
warning: 'failed to mark current claimed
|
|
394
|
+
warning: 'failed to mark current claimed tasks failed during daemon shutdown',
|
|
368
395
|
signal,
|
|
369
396
|
error: errorMessage(error),
|
|
370
397
|
}, null, 2))
|
|
@@ -908,8 +935,8 @@ async function prepareRuntimeTask(task, flags, deps, config) {
|
|
|
908
935
|
const database = await syncDatabaseIfNeeded({ task, workdir, config, flags, deps })
|
|
909
936
|
const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
|
|
910
937
|
|
|
911
|
-
// Resolve and inject the project token so MCP
|
|
912
|
-
//
|
|
938
|
+
// Resolve and inject the project token so hosted MCP calls spawned by the
|
|
939
|
+
// runtime agent can authenticate without needing credentials on disk.
|
|
913
940
|
const projectToken = await projectSyncToken(projectTokenCandidatesForTask(task, flags), flags, deps)
|
|
914
941
|
const env = {
|
|
915
942
|
...process.env,
|
|
@@ -1228,156 +1255,160 @@ function defaultTaskHandler(flags, deps) {
|
|
|
1228
1255
|
}
|
|
1229
1256
|
}
|
|
1230
1257
|
|
|
1231
|
-
async function
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (!runtime?.id) continue
|
|
1239
|
-
const claim = await runWithDaemonRetry('claim runtime task', () => (
|
|
1240
|
-
deps.requestJson('/api/runtime/tasks/claim', {
|
|
1241
|
-
method: 'POST',
|
|
1242
|
-
body: {
|
|
1243
|
-
runtime_id: runtime.id,
|
|
1244
|
-
machine_key: machineKey,
|
|
1245
|
-
},
|
|
1246
|
-
config,
|
|
1247
|
-
})
|
|
1248
|
-
), deps, retryState)
|
|
1249
|
-
const task = claim?.task
|
|
1250
|
-
if (!task) continue
|
|
1251
|
-
|
|
1252
|
-
const runtimeTask = {
|
|
1253
|
-
...task,
|
|
1254
|
-
runtime_id: task.runtime_id || runtime.id,
|
|
1255
|
-
runtime: {
|
|
1256
|
-
provider: runtime.provider,
|
|
1257
|
-
model: runtime.model || '',
|
|
1258
|
-
...(task.runtime || {}),
|
|
1259
|
-
id: task.runtime?.id || runtime.id,
|
|
1260
|
-
machine_key: task.runtime?.machine_key || machineKey,
|
|
1258
|
+
async function runOneRuntimeTaskSlot(runtime, config, machineKey, registration, flags, deps, handlerModule, retryState, heartbeatIntervalMs) {
|
|
1259
|
+
const claim = await runWithDaemonRetry('claim runtime task', () => (
|
|
1260
|
+
deps.requestJson('/api/runtime/tasks/claim', {
|
|
1261
|
+
method: 'POST',
|
|
1262
|
+
body: {
|
|
1263
|
+
runtime_id: runtime.id,
|
|
1264
|
+
machine_key: machineKey,
|
|
1261
1265
|
},
|
|
1266
|
+
config,
|
|
1267
|
+
})
|
|
1268
|
+
), deps, retryState)
|
|
1269
|
+
const task = claim?.task
|
|
1270
|
+
if (!task) return false
|
|
1271
|
+
|
|
1272
|
+
const runtimeTask = {
|
|
1273
|
+
...task,
|
|
1274
|
+
runtime_id: task.runtime_id || runtime.id,
|
|
1275
|
+
runtime: {
|
|
1276
|
+
provider: runtime.provider,
|
|
1277
|
+
model: runtime.model || '',
|
|
1278
|
+
...(task.runtime || {}),
|
|
1279
|
+
id: task.runtime?.id || runtime.id,
|
|
1280
|
+
machine_key: task.runtime?.machine_key || machineKey,
|
|
1281
|
+
},
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
1285
|
+
await writeCurrentClaim(deps, runtimeTask, machineKey)
|
|
1286
|
+
let completion = null
|
|
1287
|
+
let executionContext = null
|
|
1288
|
+
if (runtimeTask.workspace?.slug) {
|
|
1289
|
+
const token = await projectSyncToken(projectTokenCandidatesForTask(runtimeTask, flags), flags, deps)
|
|
1290
|
+
const syncConfig = { ...config, token }
|
|
1291
|
+
try {
|
|
1292
|
+
await runWithTaskRetry('sync knowledge base', () => (
|
|
1293
|
+
deps.syncKnowledge({
|
|
1294
|
+
project: runtimeTask.workspace.slug,
|
|
1295
|
+
mode: 'pull',
|
|
1296
|
+
server: flags.server,
|
|
1297
|
+
token,
|
|
1298
|
+
}, {
|
|
1299
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config: syncConfig }),
|
|
1300
|
+
log: () => {},
|
|
1301
|
+
})
|
|
1302
|
+
), deps)
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
completion = {
|
|
1305
|
+
comment: `Knowledge sync pull failed before task execution: ${errorMessage(error)}`,
|
|
1306
|
+
status: 'failed',
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!completion) {
|
|
1311
|
+
executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
|
|
1312
|
+
runtimeTask.execution_context = executionContext
|
|
1313
|
+
await updateRunMeta(executionContext.run_dir, {
|
|
1314
|
+
task_id: String(runtimeTask.id || ''),
|
|
1315
|
+
runtime_id: String(runtimeTask.runtime_id || ''),
|
|
1316
|
+
provider: runtimeTask.runtime?.provider || runtime.provider || '',
|
|
1317
|
+
project_slug: executionContext.project_slug,
|
|
1318
|
+
agent: agentNameForTask(runtimeTask),
|
|
1319
|
+
issue_title: runtimeTask.issue?.title || '',
|
|
1320
|
+
started_at: new Date().toISOString(),
|
|
1321
|
+
})
|
|
1322
|
+
try {
|
|
1323
|
+
completion = await runWithRuntimeHeartbeat(
|
|
1324
|
+
() => handlerModule.handleRuntimeTask(runtimeTask),
|
|
1325
|
+
registration,
|
|
1326
|
+
flags,
|
|
1327
|
+
deps,
|
|
1328
|
+
heartbeatIntervalMs
|
|
1329
|
+
)
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
completion = {
|
|
1332
|
+
comment: error instanceof Error ? error.message : String(error),
|
|
1333
|
+
status: 'failed',
|
|
1334
|
+
}
|
|
1335
|
+
} finally {
|
|
1336
|
+
await rm(executionContext.tmp_dir, { recursive: true, force: true })
|
|
1262
1337
|
}
|
|
1338
|
+
}
|
|
1339
|
+
if (executionContext) {
|
|
1340
|
+
await writeRunFile(executionContext.run_dir, 'completion.json', JSON.stringify(normalizeTaskCompletion(runtimeTask, completion), null, 2))
|
|
1341
|
+
await updateRunMeta(executionContext.run_dir, { ended_at: new Date().toISOString() })
|
|
1342
|
+
}
|
|
1343
|
+
if (executionContext) {
|
|
1344
|
+
await appendTaskMemoryDelta({ task: runtimeTask, completion, workdir: executionContext.workdir })
|
|
1345
|
+
}
|
|
1263
1346
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1347
|
+
if (executionContext && runtimeTask.workspace?.slug) {
|
|
1348
|
+
const isDeepOrganize = Boolean(knowledgeDeepOrganizeSpec(runtimeTask))
|
|
1349
|
+
const syncBack = isDeepOrganize ? deps.mcpKnowledgeSync : deps.syncKnowledge
|
|
1350
|
+
const token = await projectSyncToken(projectTokenCandidatesForTask(runtimeTask, flags), flags, deps)
|
|
1351
|
+
const syncConfig = { ...config, token }
|
|
1352
|
+
const pushKnowledge = () => syncBack({
|
|
1353
|
+
project: runtimeTask.workspace.slug,
|
|
1354
|
+
mode: 'push',
|
|
1355
|
+
server: flags.server,
|
|
1356
|
+
token,
|
|
1357
|
+
}, {
|
|
1358
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config: syncConfig }),
|
|
1359
|
+
log: () => {},
|
|
1360
|
+
})
|
|
1361
|
+
if (isDeepOrganize) {
|
|
1271
1362
|
try {
|
|
1272
|
-
await
|
|
1273
|
-
deps.syncKnowledge({
|
|
1274
|
-
project: runtimeTask.workspace.slug,
|
|
1275
|
-
mode: 'pull',
|
|
1276
|
-
server: flags.server,
|
|
1277
|
-
token,
|
|
1278
|
-
}, {
|
|
1279
|
-
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config: syncConfig }),
|
|
1280
|
-
log: () => {},
|
|
1281
|
-
})
|
|
1282
|
-
), deps)
|
|
1363
|
+
await pushKnowledge()
|
|
1283
1364
|
} catch (error) {
|
|
1365
|
+
const message = `Knowledge sync push failed before task completion: ${errorMessage(error)}. If a knowledge_snapshot is included in completion, the platform will write it directly.`
|
|
1284
1366
|
completion = {
|
|
1285
|
-
|
|
1286
|
-
|
|
1367
|
+
...(completion && typeof completion === 'object' ? completion : {}),
|
|
1368
|
+
comment: [String(completion?.comment || completion?.summary || '').trim(), message].filter(Boolean).join('\n\n'),
|
|
1369
|
+
memory_delta: [String(completion?.memory_delta || completion?.memoryDelta || '').trim(), message].filter(Boolean).join('\n'),
|
|
1287
1370
|
}
|
|
1288
1371
|
}
|
|
1289
|
-
}
|
|
1290
|
-
if (!completion) {
|
|
1291
|
-
executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
|
|
1292
|
-
runtimeTask.execution_context = executionContext
|
|
1293
|
-
await updateRunMeta(executionContext.run_dir, {
|
|
1294
|
-
task_id: String(runtimeTask.id || ''),
|
|
1295
|
-
runtime_id: String(runtimeTask.runtime_id || ''),
|
|
1296
|
-
provider: runtimeTask.runtime?.provider || runtime.provider || '',
|
|
1297
|
-
project_slug: executionContext.project_slug,
|
|
1298
|
-
agent: agentNameForTask(runtimeTask),
|
|
1299
|
-
issue_title: runtimeTask.issue?.title || '',
|
|
1300
|
-
started_at: new Date().toISOString(),
|
|
1301
|
-
})
|
|
1372
|
+
} else {
|
|
1302
1373
|
try {
|
|
1303
|
-
|
|
1304
|
-
() => handlerModule.handleRuntimeTask(runtimeTask),
|
|
1305
|
-
registration,
|
|
1306
|
-
flags,
|
|
1307
|
-
deps,
|
|
1308
|
-
heartbeatIntervalMs
|
|
1309
|
-
)
|
|
1374
|
+
await runWithTaskRetry('sync knowledge base back to cloud', pushKnowledge, deps)
|
|
1310
1375
|
} catch (error) {
|
|
1376
|
+
const message = `Knowledge sync push failed before task completion: ${errorMessage(error)}`
|
|
1311
1377
|
completion = {
|
|
1312
|
-
|
|
1378
|
+
...(completion && typeof completion === 'object' ? completion : {}),
|
|
1379
|
+
comment: [String(completion?.comment || completion?.summary || '').trim(), message].filter(Boolean).join('\n\n'),
|
|
1380
|
+
memory_delta: [String(completion?.memory_delta || completion?.memoryDelta || '').trim(), message].filter(Boolean).join('\n'),
|
|
1313
1381
|
status: 'failed',
|
|
1314
1382
|
}
|
|
1315
|
-
} finally {
|
|
1316
|
-
await rm(executionContext.tmp_dir, { recursive: true, force: true })
|
|
1317
1383
|
}
|
|
1318
1384
|
}
|
|
1319
|
-
|
|
1320
|
-
await writeRunFile(executionContext.run_dir, 'completion.json', JSON.stringify(normalizeTaskCompletion(runtimeTask, completion), null, 2))
|
|
1321
|
-
await updateRunMeta(executionContext.run_dir, { ended_at: new Date().toISOString() })
|
|
1322
|
-
}
|
|
1323
|
-
if (executionContext) {
|
|
1324
|
-
await appendTaskMemoryDelta({ task: runtimeTask, completion, workdir: executionContext.workdir })
|
|
1325
|
-
}
|
|
1385
|
+
}
|
|
1326
1386
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
log: () => {},
|
|
1340
|
-
})
|
|
1341
|
-
if (isDeepOrganize) {
|
|
1342
|
-
try {
|
|
1343
|
-
await pushKnowledge()
|
|
1344
|
-
} catch (error) {
|
|
1345
|
-
const message = `Knowledge sync push failed before task completion: ${errorMessage(error)}. If a knowledge_snapshot is included in completion, the platform will write it directly.`
|
|
1346
|
-
completion = {
|
|
1347
|
-
...(completion && typeof completion === 'object' ? completion : {}),
|
|
1348
|
-
comment: [String(completion?.comment || completion?.summary || '').trim(), message].filter(Boolean).join('\n\n'),
|
|
1349
|
-
memory_delta: [String(completion?.memory_delta || completion?.memoryDelta || '').trim(), message].filter(Boolean).join('\n'),
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
} else {
|
|
1353
|
-
try {
|
|
1354
|
-
await runWithTaskRetry('sync knowledge base back to cloud', pushKnowledge, deps)
|
|
1355
|
-
} catch (error) {
|
|
1356
|
-
const message = `Knowledge sync push failed before task completion: ${errorMessage(error)}`
|
|
1357
|
-
completion = {
|
|
1358
|
-
...(completion && typeof completion === 'object' ? completion : {}),
|
|
1359
|
-
comment: [String(completion?.comment || completion?.summary || '').trim(), message].filter(Boolean).join('\n\n'),
|
|
1360
|
-
memory_delta: [String(completion?.memory_delta || completion?.memoryDelta || '').trim(), message].filter(Boolean).join('\n'),
|
|
1361
|
-
status: 'failed',
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1387
|
+
const body = normalizeTaskCompletion(runtimeTask, completion)
|
|
1388
|
+
const result = await runWithDaemonRetry('complete runtime task', () => (
|
|
1389
|
+
deps.requestJson('/api/runtime/tasks/complete', {
|
|
1390
|
+
method: 'POST',
|
|
1391
|
+
body,
|
|
1392
|
+
config,
|
|
1393
|
+
})
|
|
1394
|
+
), deps, retryState)
|
|
1395
|
+
await clearCurrentClaim(deps, runtimeTask.id)
|
|
1396
|
+
deps.log(JSON.stringify(result, null, 2))
|
|
1397
|
+
return true
|
|
1398
|
+
}
|
|
1366
1399
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
})
|
|
1374
|
-
), deps, retryState)
|
|
1375
|
-
await clearCurrentClaim(deps)
|
|
1376
|
-
deps.log(JSON.stringify(result, null, 2))
|
|
1377
|
-
handled += 1
|
|
1378
|
-
}
|
|
1400
|
+
async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule, retryState = createRetryState(), heartbeatIntervalMs = 15000, maxConcurrent = 1) {
|
|
1401
|
+
if (!handlerModule) return 0
|
|
1402
|
+
const runtimes = (registration?.runtimes || []).filter(r => r?.id)
|
|
1403
|
+
if (!runtimes.length) return 0
|
|
1404
|
+
const config = configFromFlags(flags)
|
|
1405
|
+
const machineKey = registration?.machine?.machine_key || machineOverride(flags) || ''
|
|
1379
1406
|
|
|
1380
|
-
|
|
1407
|
+
const slots = Array.from({ length: maxConcurrent }, (_, i) => runtimes[i % runtimes.length])
|
|
1408
|
+
const results = await Promise.allSettled(
|
|
1409
|
+
slots.map(runtime => runOneRuntimeTaskSlot(runtime, config, machineKey, registration, flags, deps, handlerModule, retryState, heartbeatIntervalMs))
|
|
1410
|
+
)
|
|
1411
|
+
return results.filter(r => r.status === 'fulfilled' && r.value === true).length
|
|
1381
1412
|
}
|
|
1382
1413
|
|
|
1383
1414
|
export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
@@ -1386,6 +1417,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1386
1417
|
const scanIntervalMs = Number(flag(flags, 'scan-interval', '60')) * 1000
|
|
1387
1418
|
const taskIntervalMs = Number(flag(flags, 'task-interval', flag(flags, 'heartbeat-interval', '15'))) * 1000
|
|
1388
1419
|
const projectRefreshIntervalMs = Number(flag(flags, 'project-refresh-interval', '1800')) * 1000
|
|
1420
|
+
const maxConcurrent = Math.max(1, Number(flag(flags, 'concurrency', '1')) || 1)
|
|
1389
1421
|
const once = Boolean(flags.once)
|
|
1390
1422
|
const handlerPath = flag(flags, 'handler')
|
|
1391
1423
|
|
|
@@ -1407,13 +1439,13 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1407
1439
|
const uninstallExitHandlers = installCurrentClaimExitHandlers(flags, resolvedDeps)
|
|
1408
1440
|
try {
|
|
1409
1441
|
let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1410
|
-
await runWithDaemonRetry('recover current claimed task', () =>
|
|
1442
|
+
await runWithDaemonRetry('recover current claimed task', () => failAllPersistedClaims(
|
|
1411
1443
|
flags,
|
|
1412
1444
|
resolvedDeps,
|
|
1413
1445
|
'Runtime task failed locally: daemon restarted with a persisted claimed task that had not completed.'
|
|
1414
1446
|
), resolvedDeps, retryState)
|
|
1415
1447
|
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
1416
|
-
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1448
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs, maxConcurrent)
|
|
1417
1449
|
if (once) return
|
|
1418
1450
|
|
|
1419
1451
|
let lastScan = Date.now()
|
|
@@ -1432,7 +1464,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1432
1464
|
lastHeartbeat = now
|
|
1433
1465
|
}
|
|
1434
1466
|
if (now - lastTaskPoll >= taskIntervalMs) {
|
|
1435
|
-
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1467
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs, maxConcurrent)
|
|
1436
1468
|
lastTaskPoll = now
|
|
1437
1469
|
}
|
|
1438
1470
|
if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
|
|
@@ -1442,7 +1474,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1442
1474
|
}
|
|
1443
1475
|
} catch (error) {
|
|
1444
1476
|
try {
|
|
1445
|
-
await
|
|
1477
|
+
await failAllPersistedClaims(
|
|
1446
1478
|
flags,
|
|
1447
1479
|
resolvedDeps,
|
|
1448
1480
|
`Runtime task failed locally: daemon exited with error before the claimed task completed: ${errorMessage(error)}`
|