gitdone-agent 0.7.5 → 0.7.6
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 +83 -13
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
27
27
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
28
28
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
29
29
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
30
|
-
const AGENT_VERSION = '0.7.
|
|
30
|
+
const AGENT_VERSION = '0.7.6'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -909,7 +909,7 @@ async function reportCommandResult(cfg, id, status, result) {
|
|
|
909
909
|
// how many tokens the run cost. `streamingText` / `activityText` (gd-419) carry
|
|
910
910
|
// the live "being typed" preview and the latest thinking snippet — explicit ''
|
|
911
911
|
// clears them; `undefined` leaves them untouched.
|
|
912
|
-
async function postRunEvents(cfg, runId, events, status, result, usage, streamingText, activityText) {
|
|
912
|
+
async function postRunEvents(cfg, runId, events, status, result, usage, streamingText, activityText, extra) {
|
|
913
913
|
await api(cfg, '/api/v1/agent/ai-run/events', {
|
|
914
914
|
runId,
|
|
915
915
|
events,
|
|
@@ -918,9 +918,32 @@ async function postRunEvents(cfg, runId, events, status, result, usage, streamin
|
|
|
918
918
|
...(usage ? { usage } : {}),
|
|
919
919
|
...(streamingText !== undefined ? { streamingText } : {}),
|
|
920
920
|
...(activityText !== undefined ? { activityText } : {}),
|
|
921
|
+
// gd-466: claude's own session id (for --resume) + precise stop cause.
|
|
922
|
+
...(extra?.claudeSessionId ? { claudeSessionId: extra.claudeSessionId } : {}),
|
|
923
|
+
...(extra?.stopReason ? { stopReason: extra.stopReason } : {}),
|
|
924
|
+
...(typeof extra?.resetAt === 'number' ? { resetAt: extra.resetAt } : {}),
|
|
921
925
|
}).catch((e) => log(`✗ ai-run events post failed: ${e.message}`))
|
|
922
926
|
}
|
|
923
927
|
|
|
928
|
+
// gd-466: parse a hard usage-limit stop out of claude's headless output. On the
|
|
929
|
+
// limit, the CLI surfaces "Claude AI usage limit reached|<epoch_seconds>" (in
|
|
930
|
+
// the result text and/or stderr); the epoch is the window reset. Returns the
|
|
931
|
+
// reset in ms when found, else { limited } with a null resetAt (telemetry is
|
|
932
|
+
// then the backstop), else not limited.
|
|
933
|
+
function detectUsageLimit(text) {
|
|
934
|
+
const hay = String(text || '')
|
|
935
|
+
const withEpoch = hay.match(/usage limit reached\s*\|\s*(\d{9,13})/i)
|
|
936
|
+
if (withEpoch) {
|
|
937
|
+
let n = Number(withEpoch[1])
|
|
938
|
+
if (n < 1e12) n *= 1000 // seconds → ms
|
|
939
|
+
return { limited: true, resetAt: n }
|
|
940
|
+
}
|
|
941
|
+
if (/usage limit reached|reached your usage limit|rate limit(?:ed)?|exceeded your.*\blimit\b/i.test(hay)) {
|
|
942
|
+
return { limited: true, resetAt: null }
|
|
943
|
+
}
|
|
944
|
+
return { limited: false, resetAt: null }
|
|
945
|
+
}
|
|
946
|
+
|
|
924
947
|
// Post transcript lines (and optional turn status / Claude session id) for an
|
|
925
948
|
// interactive AiSession chat turn.
|
|
926
949
|
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText, activityText) {
|
|
@@ -991,6 +1014,11 @@ function parseStreamLine(line, push, onInit, onDelta, onMeta, onTurnEnd) {
|
|
|
991
1014
|
}
|
|
992
1015
|
if (ev.type === 'result') {
|
|
993
1016
|
if (ev.subtype && ev.subtype !== 'success') push('SYSTEM', `Резултат: ${ev.subtype}`)
|
|
1017
|
+
// gd-466: surface the final result text + subtype so the caller can detect a
|
|
1018
|
+
// hard usage-limit stop (the "usage limit reached|<epoch>" lands here).
|
|
1019
|
+
if (typeof onMeta === 'function' && (ev.subtype || typeof ev.result === 'string')) {
|
|
1020
|
+
onMeta({ subtype: ev.subtype, resultText: typeof ev.result === 'string' ? ev.result : undefined })
|
|
1021
|
+
}
|
|
994
1022
|
// claude's final result carries cumulative token usage + its own cost.
|
|
995
1023
|
// Prefer `modelUsage` (summed over every model/subagent turn) which is the
|
|
996
1024
|
// true cumulative; top-level `usage` is often just the last turn. Fall back
|
|
@@ -1176,6 +1204,8 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1176
1204
|
const prompt = cmd.payload?.prompt ?? ''
|
|
1177
1205
|
const allowCommit = cmd.payload?.allowCommit === true
|
|
1178
1206
|
const model = aiModelArg(cmd.payload?.model)
|
|
1207
|
+
// gd-466: a token-reset resume asks us to continue claude's own conversation.
|
|
1208
|
+
const resumeId = cmd.payload?.resume || null
|
|
1179
1209
|
if (!runId || !prompt) {
|
|
1180
1210
|
reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_run payload')
|
|
1181
1211
|
return
|
|
@@ -1208,6 +1238,10 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1208
1238
|
// Stream text token-by-token + thinking deltas so the terminal shows the
|
|
1209
1239
|
// reply being written live, ред по ред, not in whole-block batches (gd-419).
|
|
1210
1240
|
'--include-partial-messages',
|
|
1241
|
+
// gd-466: resume claude's own prior conversation for this task when we're
|
|
1242
|
+
// continuing after a token reset — it keeps its context/todo. A stale/missing
|
|
1243
|
+
// session errors out fast; the watchdog then re-dispatches fresh (no resume).
|
|
1244
|
+
...(resumeId ? ['--resume', resumeId] : []),
|
|
1211
1245
|
// Model chosen in gitDone (project default / task); omitted → machine default (gd-354).
|
|
1212
1246
|
...(model ? ['--model', model] : []),
|
|
1213
1247
|
'--permission-mode', 'acceptEdits',
|
|
@@ -1240,7 +1274,11 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1240
1274
|
const activityText = activityChanged ? liveActivity : undefined
|
|
1241
1275
|
sentLive = liveText
|
|
1242
1276
|
sentActivity = liveActivity
|
|
1243
|
-
|
|
1277
|
+
// gd-466: persist claude's session id as soon as we have it (once), so a
|
|
1278
|
+
// resume survives even if the agent dies before the clean close handler.
|
|
1279
|
+
const extra = (!sentSession && capturedSession) ? { claudeSessionId: capturedSession } : undefined
|
|
1280
|
+
if (extra) sentSession = true
|
|
1281
|
+
await postRunEvents(cfg, runId, batch, undefined, undefined, undefined, streamingText, activityText, extra)
|
|
1244
1282
|
flushing = false
|
|
1245
1283
|
}
|
|
1246
1284
|
const push = (kind, text) => {
|
|
@@ -1260,14 +1298,25 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1260
1298
|
const timer = setInterval(flush, 500)
|
|
1261
1299
|
|
|
1262
1300
|
// 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
|
-
|
|
1301
|
+
// from the final result event) to report once on exit (gd-334). gd-466 also
|
|
1302
|
+
// captures the final result text/subtype (for usage-limit detection).
|
|
1303
|
+
const meta = { model: undefined, usage: undefined, costUsd: undefined, resultText: undefined, subtype: undefined }
|
|
1265
1304
|
const onMeta = (m) => {
|
|
1266
1305
|
if (m.model) meta.model = m.model
|
|
1267
1306
|
if (m.usage) meta.usage = m.usage
|
|
1268
1307
|
if (typeof m.costUsd === 'number') meta.costUsd = m.costUsd
|
|
1308
|
+
if (m.resultText) meta.resultText = m.resultText
|
|
1309
|
+
if (m.subtype) meta.subtype = m.subtype
|
|
1269
1310
|
}
|
|
1270
1311
|
|
|
1312
|
+
// gd-466: claude's own session id (from its init event) + any stderr — both
|
|
1313
|
+
// feed a token-reset resume: the id lets us --resume, the stderr helps detect
|
|
1314
|
+
// a usage-limit stop.
|
|
1315
|
+
let capturedSession = null
|
|
1316
|
+
let sentSession = false
|
|
1317
|
+
const onInit = (sid) => { if (sid) capturedSession = sid }
|
|
1318
|
+
let stderrBuf = ''
|
|
1319
|
+
|
|
1271
1320
|
log(`▶ ai_run ${runId} @ ${repoPath} via ${claudePath}`)
|
|
1272
1321
|
postRunEvents(cfg, runId, [{ kind: 'SYSTEM', text: `Стартиране на Claude Code в ${repoPath}…` }], 'running')
|
|
1273
1322
|
|
|
@@ -1296,14 +1345,14 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1296
1345
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
1297
1346
|
const line = buf.slice(0, nl).trim()
|
|
1298
1347
|
buf = buf.slice(nl + 1)
|
|
1299
|
-
if (line) parseStreamLine(line, push,
|
|
1348
|
+
if (line) parseStreamLine(line, push, onInit, onDelta, onMeta)
|
|
1300
1349
|
}
|
|
1301
1350
|
})
|
|
1302
|
-
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
|
|
1351
|
+
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) { stderrBuf = (stderrBuf + '\n' + s).slice(-8000); push('SYSTEM', s) } })
|
|
1303
1352
|
child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
|
|
1304
1353
|
child.on('close', async (code) => {
|
|
1305
1354
|
clearInterval(timer)
|
|
1306
|
-
if (buf.trim()) parseStreamLine(buf.trim(), push,
|
|
1355
|
+
if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta, onMeta)
|
|
1307
1356
|
liveText = '' // run is over — drop any lingering live preview / thought
|
|
1308
1357
|
liveActivity = ''
|
|
1309
1358
|
await flush()
|
|
@@ -1312,17 +1361,38 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1312
1361
|
const usage = meta.usage
|
|
1313
1362
|
? { model: meta.model, ...meta.usage, ...(typeof meta.costUsd === 'number' ? { costUsd: meta.costUsd } : {}) }
|
|
1314
1363
|
: undefined
|
|
1315
|
-
|
|
1364
|
+
|
|
1365
|
+
// gd-466: did we stop on the token limit? Check the final result text, its
|
|
1366
|
+
// subtype, and stderr. A hard hit gives us the precise reset epoch; report
|
|
1367
|
+
// it so the server parks the task for an exact auto-resume.
|
|
1368
|
+
const limit = detectUsageLimit(`${meta.resultText || ''}\n${meta.subtype || ''}\n${stderrBuf}`)
|
|
1369
|
+
const extra = {
|
|
1370
|
+
...(capturedSession ? { claudeSessionId: capturedSession } : {}),
|
|
1371
|
+
...(limit.limited ? { stopReason: 'usage_limit' } : {}),
|
|
1372
|
+
...(limit.limited && typeof limit.resetAt === 'number' ? { resetAt: limit.resetAt } : {}),
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const events = [{
|
|
1376
|
+
kind: 'SYSTEM',
|
|
1377
|
+
text: limit.limited
|
|
1378
|
+
? '⏳ Достигнат лимит на токени — спирам. gitDone ще ме продължи автоматично след ресета.'
|
|
1379
|
+
: (ok ? '✓ Готово.' : `✗ Процесът приключи с код ${code}.`),
|
|
1380
|
+
}]
|
|
1316
1381
|
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
1382
|
await postRunEvents(
|
|
1318
1383
|
cfg, runId,
|
|
1319
1384
|
events,
|
|
1320
|
-
|
|
1321
|
-
|
|
1385
|
+
// A limit stop is not a clean finish — report it as 'error' so the run
|
|
1386
|
+
// ends and the server's park/resume path (not the DONE path) takes over.
|
|
1387
|
+
ok && !limit.limited ? 'done' : 'error',
|
|
1388
|
+
limit.limited ? 'usage_limit' : (ok ? 'ok' : `exit ${code}`),
|
|
1322
1389
|
usage,
|
|
1390
|
+
undefined,
|
|
1391
|
+
undefined,
|
|
1392
|
+
extra,
|
|
1323
1393
|
)
|
|
1324
|
-
reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
|
|
1325
|
-
log(`■ ai_run ${runId} приключи (code ${code})`)
|
|
1394
|
+
reportCommandResult(cfg, cmd.id, ok && !limit.limited ? 'done' : 'error', limit.limited ? 'usage_limit' : `exit ${code}`)
|
|
1395
|
+
log(`■ ai_run ${runId} приключи (code ${code}${limit.limited ? ', usage_limit' : ''})`)
|
|
1326
1396
|
})
|
|
1327
1397
|
}
|
|
1328
1398
|
|