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.
Files changed (2) hide show
  1. package/index.js +83 -13
  2. 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.5'
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
- await postRunEvents(cfg, runId, batch, undefined, undefined, undefined, streamingText, activityText)
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
- const meta = { model: undefined, usage: undefined, costUsd: undefined }
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, undefined, onDelta, onMeta)
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, undefined, onDelta, onMeta)
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
- const events = [{ kind: 'SYSTEM', text: ok ? '✓ Готово.' : `✗ Процесът приключи с код ${code}.` }]
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
- ok ? 'done' : 'error',
1321
- ok ? 'ok' : `exit ${code}`,
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
5
  "type": "module",
6
6
  "bin": {