gitdone-agent 0.7.5 → 0.7.7
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/index.js +183 -21
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { execSync, execFileSync, spawn } from 'node:child_process'
|
|
|
13
13
|
import {
|
|
14
14
|
existsSync, writeFileSync, readFileSync, unlinkSync, renameSync,
|
|
15
15
|
mkdirSync, copyFileSync, appendFileSync, readdirSync, rmSync, statSync,
|
|
16
|
+
openSync, writeSync, fsyncSync, closeSync,
|
|
16
17
|
} from 'node:fs'
|
|
17
18
|
import { resolve, join } from 'node:path'
|
|
18
19
|
import { homedir, hostname, tmpdir } from 'node:os'
|
|
@@ -27,10 +28,12 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
27
28
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
28
29
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
29
30
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
30
|
-
const AGENT_VERSION = '0.7.
|
|
31
|
+
const AGENT_VERSION = '0.7.7'
|
|
31
32
|
|
|
32
33
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
34
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
35
|
+
// Last-known-good copy of config.json, refreshed once per successful start.
|
|
36
|
+
const CONFIG_BAK_PATH = CONFIG_PATH + '.bak'
|
|
34
37
|
const STABLE_AGENT = join(AGENT_DIR, 'agent.mjs')
|
|
35
38
|
const LOG_PATH = join(AGENT_DIR, 'agent.log')
|
|
36
39
|
|
|
@@ -80,20 +83,98 @@ function survive(kind, err) {
|
|
|
80
83
|
process.on('uncaughtException', (err) => survive('uncaught exception', err))
|
|
81
84
|
process.on('unhandledRejection', (err) => survive('unhandled rejection', err))
|
|
82
85
|
|
|
83
|
-
function
|
|
84
|
-
try {
|
|
86
|
+
function parseConfigFile(path) {
|
|
87
|
+
try {
|
|
88
|
+
// Strip a UTF-8 BOM: we never write one, but a config repaired by hand in an
|
|
89
|
+
// editor that adds one would otherwise be unparsable — and "I fixed the file
|
|
90
|
+
// and it still says no config" is the worst possible dead end (gd-478).
|
|
91
|
+
const cfg = JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''))
|
|
92
|
+
return cfg && typeof cfg === 'object' && cfg.key ? cfg : null
|
|
93
|
+
} catch { return null }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Rebuild the bare minimum from ai-mcp-config.json — last resort when both
|
|
97
|
+
// config.json and its backup are gone. It is written from cfg.key/cfg.machineId
|
|
98
|
+
// on every AI session, so it carries the two values that cannot be regenerated
|
|
99
|
+
// locally (a new machineId would show up as a SECOND machine on the server, and
|
|
100
|
+
// there is no way to invent the API key). Roots are lost, but an agent that is
|
|
101
|
+
// online with no roots can be fixed from the web UI; an offline one cannot.
|
|
102
|
+
function recoverConfigFromAiMcp() {
|
|
103
|
+
try {
|
|
104
|
+
const mcp = JSON.parse(readFileSync(join(AGENT_DIR, 'ai-mcp-config.json'), 'utf8'))
|
|
105
|
+
const server = mcp?.mcpServers?.gitdone
|
|
106
|
+
const key = server?.headers?.Authorization?.replace(/^Bearer\s+/, '')
|
|
107
|
+
const machineId = server?.headers?.['X-Gitdone-Machine-Id']
|
|
108
|
+
if (!key || !machineId || !server.url) return null
|
|
109
|
+
return {
|
|
110
|
+
key,
|
|
111
|
+
url: server.url.replace(/\/api\/mcp\/?$/, ''),
|
|
112
|
+
interval: 30,
|
|
113
|
+
machineId,
|
|
114
|
+
hostname: hostname(),
|
|
115
|
+
roots: [],
|
|
116
|
+
}
|
|
117
|
+
} catch { return null }
|
|
85
118
|
}
|
|
86
119
|
|
|
120
|
+
// True when the running config had to be rebuilt — surfaced by --doctor.
|
|
121
|
+
let configWasRecovered = null
|
|
122
|
+
|
|
123
|
+
// An unclean shutdown (power loss, BSOD) can leave config.json committed by the
|
|
124
|
+
// filesystem as a NUL-filled file of the right length: intact on disk, garbage
|
|
125
|
+
// to JSON.parse. Before gd-478 that bricked the agent for good — every autostart
|
|
126
|
+
// exited with "no config found" and the supervisor just respawned it every 10s,
|
|
127
|
+
// so the machine stayed offline until somebody hand-edited the file. A user
|
|
128
|
+
// without filesystem access had no way out at all. So never trust the primary
|
|
129
|
+
// copy blindly: fall back to the last-known-good backup, then to the AI MCP
|
|
130
|
+
// config, and put whichever survived back in place.
|
|
131
|
+
function readConfig() {
|
|
132
|
+
const primary = parseConfigFile(CONFIG_PATH)
|
|
133
|
+
if (primary) return primary
|
|
134
|
+
// Nothing to recover from on a fresh machine (install path) — stay quiet.
|
|
135
|
+
if (!existsSync(CONFIG_PATH) && !existsSync(CONFIG_BAK_PATH)) return null
|
|
136
|
+
|
|
137
|
+
for (const [source, cfg] of [
|
|
138
|
+
['config.json.bak', parseConfigFile(CONFIG_BAK_PATH)],
|
|
139
|
+
['ai-mcp-config.json', recoverConfigFromAiMcp()],
|
|
140
|
+
]) {
|
|
141
|
+
if (!cfg) continue
|
|
142
|
+
configWasRecovered = source
|
|
143
|
+
log(`✗ config.json е повреден (най-вероятно от неочаквано изключване) — възстановен от ${source}`)
|
|
144
|
+
try { writeConfig(cfg) } catch (err) { log(`✗ записът на възстановения config не успя: ${err.message}`) }
|
|
145
|
+
return cfg
|
|
146
|
+
}
|
|
147
|
+
log('✗ config.json е повреден и няма от какво да се възстанови — пусни gitdone-agent --key=... --root=... --install')
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Atomic AND durable: write to a temp file, force it to the platter, and only
|
|
152
|
+
// then rename over the real one. The fsync is the part that matters — without
|
|
153
|
+
// it the rename can be committed while the data is still in the page cache, and
|
|
154
|
+
// a power cut in that window is exactly what produced the NUL-filled config in
|
|
155
|
+
// gd-478. Losing the rename is harmless (the old config survives); losing the
|
|
156
|
+
// data is not.
|
|
87
157
|
function writeConfig(cfg) {
|
|
88
158
|
ensureAgentDir()
|
|
89
|
-
// Write-then-rename so a crash / reboot mid-write can never leave a truncated
|
|
90
|
-
// config.json — a corrupt config makes every subsequent autostart exit
|
|
91
|
-
// immediately, which reads as "агентът умря и рестартът не помага" (gd-403).
|
|
92
159
|
const tmp = CONFIG_PATH + '.tmp'
|
|
93
|
-
|
|
160
|
+
const fd = openSync(tmp, 'w')
|
|
161
|
+
try {
|
|
162
|
+
writeSync(fd, JSON.stringify(cfg, null, 2), null, 'utf8')
|
|
163
|
+
fsyncSync(fd)
|
|
164
|
+
} finally {
|
|
165
|
+
closeSync(fd)
|
|
166
|
+
}
|
|
94
167
|
renameSync(tmp, CONFIG_PATH)
|
|
95
168
|
}
|
|
96
169
|
|
|
170
|
+
// Refresh the last-known-good copy. Called once per successful start, so the
|
|
171
|
+
// backup is always a config that actually booted the agent — never a half-baked
|
|
172
|
+
// one. Best-effort: a failed backup must not stop the agent from running.
|
|
173
|
+
function backupConfig(cfg) {
|
|
174
|
+
try { writeFileSync(CONFIG_BAK_PATH, JSON.stringify(cfg, null, 2), 'utf8') }
|
|
175
|
+
catch (err) { log(`✗ backup на config.json не успя: ${err.message}`) }
|
|
176
|
+
}
|
|
177
|
+
|
|
97
178
|
// ─── Arg parsing ─────────────────────────────────────────────────────────────
|
|
98
179
|
|
|
99
180
|
function parseArgs() {
|
|
@@ -458,7 +539,12 @@ function runDoctor() {
|
|
|
458
539
|
let hb = '(none yet)'
|
|
459
540
|
try { hb = `(последен: преди ${Math.round((Date.now() - statSync(HEARTBEAT_PATH).mtimeMs) / 1000)}s)` } catch { /* no beat yet */ }
|
|
460
541
|
console.log(` heartbeat : ${HEARTBEAT_PATH} ${hb}`)
|
|
461
|
-
|
|
542
|
+
// cfg comes from readConfig() above, so a corrupt config.json has already been
|
|
543
|
+
// repaired by the time we print this — say so rather than a bare "(ok)".
|
|
544
|
+
const cfgState = configWasRecovered ? `(ВЪЗСТАНОВЕН от ${configWasRecovered} — беше повреден)`
|
|
545
|
+
: cfg ? '(ok)' : existsSync(CONFIG_PATH) ? '(ПОВРЕДЕН)' : '(MISSING)'
|
|
546
|
+
console.log(` config : ${CONFIG_PATH} ${cfgState}`)
|
|
547
|
+
console.log(` config backup : ${CONFIG_BAK_PATH} ${parseConfigFile(CONFIG_BAK_PATH) ? '(ok)' : '(няма — прави се при първия успешен старт)'}`)
|
|
462
548
|
console.log(` log file : ${LOG_PATH} ${existsSync(LOG_PATH) ? '(ok)' : '(none yet)'}`)
|
|
463
549
|
console.log(` crash log : ${join(AGENT_DIR, 'agent-crash.log')} ${existsSync(join(AGENT_DIR, 'agent-crash.log')) ? '(има записи — виж го при проблеми)' : '(празен — няма крашове)'}`)
|
|
464
550
|
const claude = findClaude()
|
|
@@ -909,7 +995,7 @@ async function reportCommandResult(cfg, id, status, result) {
|
|
|
909
995
|
// how many tokens the run cost. `streamingText` / `activityText` (gd-419) carry
|
|
910
996
|
// the live "being typed" preview and the latest thinking snippet — explicit ''
|
|
911
997
|
// clears them; `undefined` leaves them untouched.
|
|
912
|
-
async function postRunEvents(cfg, runId, events, status, result, usage, streamingText, activityText) {
|
|
998
|
+
async function postRunEvents(cfg, runId, events, status, result, usage, streamingText, activityText, extra) {
|
|
913
999
|
await api(cfg, '/api/v1/agent/ai-run/events', {
|
|
914
1000
|
runId,
|
|
915
1001
|
events,
|
|
@@ -918,9 +1004,32 @@ async function postRunEvents(cfg, runId, events, status, result, usage, streamin
|
|
|
918
1004
|
...(usage ? { usage } : {}),
|
|
919
1005
|
...(streamingText !== undefined ? { streamingText } : {}),
|
|
920
1006
|
...(activityText !== undefined ? { activityText } : {}),
|
|
1007
|
+
// gd-466: claude's own session id (for --resume) + precise stop cause.
|
|
1008
|
+
...(extra?.claudeSessionId ? { claudeSessionId: extra.claudeSessionId } : {}),
|
|
1009
|
+
...(extra?.stopReason ? { stopReason: extra.stopReason } : {}),
|
|
1010
|
+
...(typeof extra?.resetAt === 'number' ? { resetAt: extra.resetAt } : {}),
|
|
921
1011
|
}).catch((e) => log(`✗ ai-run events post failed: ${e.message}`))
|
|
922
1012
|
}
|
|
923
1013
|
|
|
1014
|
+
// gd-466: parse a hard usage-limit stop out of claude's headless output. On the
|
|
1015
|
+
// limit, the CLI surfaces "Claude AI usage limit reached|<epoch_seconds>" (in
|
|
1016
|
+
// the result text and/or stderr); the epoch is the window reset. Returns the
|
|
1017
|
+
// reset in ms when found, else { limited } with a null resetAt (telemetry is
|
|
1018
|
+
// then the backstop), else not limited.
|
|
1019
|
+
function detectUsageLimit(text) {
|
|
1020
|
+
const hay = String(text || '')
|
|
1021
|
+
const withEpoch = hay.match(/usage limit reached\s*\|\s*(\d{9,13})/i)
|
|
1022
|
+
if (withEpoch) {
|
|
1023
|
+
let n = Number(withEpoch[1])
|
|
1024
|
+
if (n < 1e12) n *= 1000 // seconds → ms
|
|
1025
|
+
return { limited: true, resetAt: n }
|
|
1026
|
+
}
|
|
1027
|
+
if (/usage limit reached|reached your usage limit|rate limit(?:ed)?|exceeded your.*\blimit\b/i.test(hay)) {
|
|
1028
|
+
return { limited: true, resetAt: null }
|
|
1029
|
+
}
|
|
1030
|
+
return { limited: false, resetAt: null }
|
|
1031
|
+
}
|
|
1032
|
+
|
|
924
1033
|
// Post transcript lines (and optional turn status / Claude session id) for an
|
|
925
1034
|
// interactive AiSession chat turn.
|
|
926
1035
|
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText, activityText) {
|
|
@@ -991,6 +1100,11 @@ function parseStreamLine(line, push, onInit, onDelta, onMeta, onTurnEnd) {
|
|
|
991
1100
|
}
|
|
992
1101
|
if (ev.type === 'result') {
|
|
993
1102
|
if (ev.subtype && ev.subtype !== 'success') push('SYSTEM', `Резултат: ${ev.subtype}`)
|
|
1103
|
+
// gd-466: surface the final result text + subtype so the caller can detect a
|
|
1104
|
+
// hard usage-limit stop (the "usage limit reached|<epoch>" lands here).
|
|
1105
|
+
if (typeof onMeta === 'function' && (ev.subtype || typeof ev.result === 'string')) {
|
|
1106
|
+
onMeta({ subtype: ev.subtype, resultText: typeof ev.result === 'string' ? ev.result : undefined })
|
|
1107
|
+
}
|
|
994
1108
|
// claude's final result carries cumulative token usage + its own cost.
|
|
995
1109
|
// Prefer `modelUsage` (summed over every model/subagent turn) which is the
|
|
996
1110
|
// true cumulative; top-level `usage` is often just the last turn. Fall back
|
|
@@ -1176,6 +1290,8 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1176
1290
|
const prompt = cmd.payload?.prompt ?? ''
|
|
1177
1291
|
const allowCommit = cmd.payload?.allowCommit === true
|
|
1178
1292
|
const model = aiModelArg(cmd.payload?.model)
|
|
1293
|
+
// gd-466: a token-reset resume asks us to continue claude's own conversation.
|
|
1294
|
+
const resumeId = cmd.payload?.resume || null
|
|
1179
1295
|
if (!runId || !prompt) {
|
|
1180
1296
|
reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_run payload')
|
|
1181
1297
|
return
|
|
@@ -1208,6 +1324,10 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1208
1324
|
// Stream text token-by-token + thinking deltas so the terminal shows the
|
|
1209
1325
|
// reply being written live, ред по ред, not in whole-block batches (gd-419).
|
|
1210
1326
|
'--include-partial-messages',
|
|
1327
|
+
// gd-466: resume claude's own prior conversation for this task when we're
|
|
1328
|
+
// continuing after a token reset — it keeps its context/todo. A stale/missing
|
|
1329
|
+
// session errors out fast; the watchdog then re-dispatches fresh (no resume).
|
|
1330
|
+
...(resumeId ? ['--resume', resumeId] : []),
|
|
1211
1331
|
// Model chosen in gitDone (project default / task); omitted → machine default (gd-354).
|
|
1212
1332
|
...(model ? ['--model', model] : []),
|
|
1213
1333
|
'--permission-mode', 'acceptEdits',
|
|
@@ -1240,7 +1360,11 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1240
1360
|
const activityText = activityChanged ? liveActivity : undefined
|
|
1241
1361
|
sentLive = liveText
|
|
1242
1362
|
sentActivity = liveActivity
|
|
1243
|
-
|
|
1363
|
+
// gd-466: persist claude's session id as soon as we have it (once), so a
|
|
1364
|
+
// resume survives even if the agent dies before the clean close handler.
|
|
1365
|
+
const extra = (!sentSession && capturedSession) ? { claudeSessionId: capturedSession } : undefined
|
|
1366
|
+
if (extra) sentSession = true
|
|
1367
|
+
await postRunEvents(cfg, runId, batch, undefined, undefined, undefined, streamingText, activityText, extra)
|
|
1244
1368
|
flushing = false
|
|
1245
1369
|
}
|
|
1246
1370
|
const push = (kind, text) => {
|
|
@@ -1260,14 +1384,25 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1260
1384
|
const timer = setInterval(flush, 500)
|
|
1261
1385
|
|
|
1262
1386
|
// Accumulate model + token usage across the stream (model from init, usage
|
|
1263
|
-
// from the final result event) to report once on exit (gd-334).
|
|
1264
|
-
|
|
1387
|
+
// from the final result event) to report once on exit (gd-334). gd-466 also
|
|
1388
|
+
// captures the final result text/subtype (for usage-limit detection).
|
|
1389
|
+
const meta = { model: undefined, usage: undefined, costUsd: undefined, resultText: undefined, subtype: undefined }
|
|
1265
1390
|
const onMeta = (m) => {
|
|
1266
1391
|
if (m.model) meta.model = m.model
|
|
1267
1392
|
if (m.usage) meta.usage = m.usage
|
|
1268
1393
|
if (typeof m.costUsd === 'number') meta.costUsd = m.costUsd
|
|
1394
|
+
if (m.resultText) meta.resultText = m.resultText
|
|
1395
|
+
if (m.subtype) meta.subtype = m.subtype
|
|
1269
1396
|
}
|
|
1270
1397
|
|
|
1398
|
+
// gd-466: claude's own session id (from its init event) + any stderr — both
|
|
1399
|
+
// feed a token-reset resume: the id lets us --resume, the stderr helps detect
|
|
1400
|
+
// a usage-limit stop.
|
|
1401
|
+
let capturedSession = null
|
|
1402
|
+
let sentSession = false
|
|
1403
|
+
const onInit = (sid) => { if (sid) capturedSession = sid }
|
|
1404
|
+
let stderrBuf = ''
|
|
1405
|
+
|
|
1271
1406
|
log(`▶ ai_run ${runId} @ ${repoPath} via ${claudePath}`)
|
|
1272
1407
|
postRunEvents(cfg, runId, [{ kind: 'SYSTEM', text: `Стартиране на Claude Code в ${repoPath}…` }], 'running')
|
|
1273
1408
|
|
|
@@ -1296,14 +1431,14 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1296
1431
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
1297
1432
|
const line = buf.slice(0, nl).trim()
|
|
1298
1433
|
buf = buf.slice(nl + 1)
|
|
1299
|
-
if (line) parseStreamLine(line, push,
|
|
1434
|
+
if (line) parseStreamLine(line, push, onInit, onDelta, onMeta)
|
|
1300
1435
|
}
|
|
1301
1436
|
})
|
|
1302
|
-
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
|
|
1437
|
+
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) { stderrBuf = (stderrBuf + '\n' + s).slice(-8000); push('SYSTEM', s) } })
|
|
1303
1438
|
child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
|
|
1304
1439
|
child.on('close', async (code) => {
|
|
1305
1440
|
clearInterval(timer)
|
|
1306
|
-
if (buf.trim()) parseStreamLine(buf.trim(), push,
|
|
1441
|
+
if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta, onMeta)
|
|
1307
1442
|
liveText = '' // run is over — drop any lingering live preview / thought
|
|
1308
1443
|
liveActivity = ''
|
|
1309
1444
|
await flush()
|
|
@@ -1312,17 +1447,38 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1312
1447
|
const usage = meta.usage
|
|
1313
1448
|
? { model: meta.model, ...meta.usage, ...(typeof meta.costUsd === 'number' ? { costUsd: meta.costUsd } : {}) }
|
|
1314
1449
|
: undefined
|
|
1315
|
-
|
|
1450
|
+
|
|
1451
|
+
// gd-466: did we stop on the token limit? Check the final result text, its
|
|
1452
|
+
// subtype, and stderr. A hard hit gives us the precise reset epoch; report
|
|
1453
|
+
// it so the server parks the task for an exact auto-resume.
|
|
1454
|
+
const limit = detectUsageLimit(`${meta.resultText || ''}\n${meta.subtype || ''}\n${stderrBuf}`)
|
|
1455
|
+
const extra = {
|
|
1456
|
+
...(capturedSession ? { claudeSessionId: capturedSession } : {}),
|
|
1457
|
+
...(limit.limited ? { stopReason: 'usage_limit' } : {}),
|
|
1458
|
+
...(limit.limited && typeof limit.resetAt === 'number' ? { resetAt: limit.resetAt } : {}),
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const events = [{
|
|
1462
|
+
kind: 'SYSTEM',
|
|
1463
|
+
text: limit.limited
|
|
1464
|
+
? '⏳ Достигнат лимит на токени — спирам. gitDone ще ме продължи автоматично след ресета.'
|
|
1465
|
+
: (ok ? '✓ Готово.' : `✗ Процесът приключи с код ${code}.`),
|
|
1466
|
+
}]
|
|
1316
1467
|
if (usage) events.push({ kind: 'SYSTEM', text: `📊 Токени: ${fmtTokens(usage.inputTokens)} вход · ${fmtTokens(usage.outputTokens)} изход${usage.cacheReadTokens ? ` · ${fmtTokens(usage.cacheReadTokens)} кеш` : ''}${typeof usage.costUsd === 'number' ? ` · $${usage.costUsd.toFixed(4)}` : ''}` })
|
|
1317
1468
|
await postRunEvents(
|
|
1318
1469
|
cfg, runId,
|
|
1319
1470
|
events,
|
|
1320
|
-
|
|
1321
|
-
|
|
1471
|
+
// A limit stop is not a clean finish — report it as 'error' so the run
|
|
1472
|
+
// ends and the server's park/resume path (not the DONE path) takes over.
|
|
1473
|
+
ok && !limit.limited ? 'done' : 'error',
|
|
1474
|
+
limit.limited ? 'usage_limit' : (ok ? 'ok' : `exit ${code}`),
|
|
1322
1475
|
usage,
|
|
1476
|
+
undefined,
|
|
1477
|
+
undefined,
|
|
1478
|
+
extra,
|
|
1323
1479
|
)
|
|
1324
|
-
reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
|
|
1325
|
-
log(`■ ai_run ${runId} приключи (code ${code})`)
|
|
1480
|
+
reportCommandResult(cfg, cmd.id, ok && !limit.limited ? 'done' : 'error', limit.limited ? 'usage_limit' : `exit ${code}`)
|
|
1481
|
+
log(`■ ai_run ${runId} приключи (code ${code}${limit.limited ? ', usage_limit' : ''})`)
|
|
1326
1482
|
})
|
|
1327
1483
|
}
|
|
1328
1484
|
|
|
@@ -2024,6 +2180,9 @@ async function runLoop(cfg) {
|
|
|
2024
2180
|
// start so in-place-updated agents get the watchdog without a re-install.
|
|
2025
2181
|
startHeartbeat()
|
|
2026
2182
|
ensureWatchdog()
|
|
2183
|
+
// This config just booted an agent, so it is known-good — snapshot it for
|
|
2184
|
+
// readConfig() to fall back on (gd-478).
|
|
2185
|
+
backupConfig(cfg)
|
|
2027
2186
|
log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
|
|
2028
2187
|
log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
|
|
2029
2188
|
|
|
@@ -2099,7 +2258,10 @@ async function main() {
|
|
|
2099
2258
|
// No args → running mode (this is how autostart launches us): read config.
|
|
2100
2259
|
const cfg = readConfig()
|
|
2101
2260
|
if (!cfg || !cfg.key) {
|
|
2102
|
-
|
|
2261
|
+
// Through agent.log, not just stderr — the autostart runs hidden, so the
|
|
2262
|
+
// crash log is the only other place this would land and nobody reads it
|
|
2263
|
+
// until someone already suspects the agent (gd-478).
|
|
2264
|
+
log(`✗ няма използваем config в ${CONFIG_PATH} — пусни gitdone-agent --key=... --root=... --install`)
|
|
2103
2265
|
process.exit(1)
|
|
2104
2266
|
}
|
|
2105
2267
|
await runLoop(cfg)
|