From 0b5a044ea5c4fc14f1d0c66bd4d5d05719b35d9d Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Thu, 14 May 2026 19:22:40 +0200 Subject: [PATCH 1/3] feat(logs): per-job log-symlink jobs/.log -> runs/.log (IDEA-063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run-logs in /var/log/agent/runs/ zijn timestamp-named, dus de output van een specifieke job was alleen via grep te vinden. De map jobs/ bestond al maar werd niet gevuld. - run-agent.sh: geeft het run-log-pad door als RUN_LOG env-var aan run-one-job.ts. - run-one-job.ts: legt direct na de claim een symlink jobs/.log -> ../runs/.log. Relatief pad (overleeft de host bind-mount), best-effort (faalt de job nooit over een log-gemak). - log-cleanup.sh: ruimt dangling per-job symlinks op met `find -xtype l` — nodig omdat rotate-logs.sh het doel na 24u gzipt (.log -> .log.gz) of na 30d verwijdert, en de bestaande `-type f` cleanup symlinks niet raakt. Functioneel geverifieerd: symlink resolveert, dangling-prune werkt, `-type f` negeert de symlink (geen voortijdige delete). run-one-job.ts parseert schoon (node --check + type-strip). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/log-cleanup.sh | 5 +++++ bin/run-agent.sh | 4 +++- bin/run-one-job.ts | 22 +++++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/bin/log-cleanup.sh b/bin/log-cleanup.sh index dbd8a0d..5a4ad85 100755 --- a/bin/log-cleanup.sh +++ b/bin/log-cleanup.sh @@ -18,4 +18,9 @@ find "${AGENT_LOG_DIR}" -type f \ \( -name '*.log' -o -name '*.log.gz' -o -name '*.txt' -o -name '*.json' \) \ -mtime "+${AGENT_LOG_HARD_DELETE_DAYS}" -delete 2>/dev/null || true +# Prune dangling per-job symlinks: jobs/.log -> runs/.log waarvan +# het doel door rotatie is gegzipt of verwijderd. De -type f hierboven raakt +# symlinks niet, dus broken links worden hier expliciet opgeruimd (-xtype l). +find "${AGENT_LOG_DIR}/jobs" -maxdepth 1 -xtype l -delete 2>/dev/null || true + find "${AGENT_LOG_DIR}/jobs" -mindepth 1 -type d -empty -delete 2>/dev/null || true diff --git a/bin/run-agent.sh b/bin/run-agent.sh index c67213a..52c6f49 100644 --- a/bin/run-agent.sh +++ b/bin/run-agent.sh @@ -68,7 +68,9 @@ while true; do # claimt zelf via tryClaimJob, leest JobConfig (PBI-67), bouwt de # juiste Claude CLI-args, spawnt 'claude', wacht, sluit af. set +e - tsx /opt/agent/bin/run-one-job.ts > "${run_log}" 2>&1 + # RUN_LOG laat run-one-job.ts een jobs/.log symlink leggen naar + # dit run-log, zodat de output van een job op job-id vindbaar is. + RUN_LOG="${run_log}" tsx /opt/agent/bin/run-one-job.ts > "${run_log}" 2>&1 exit_code=$? set -e diff --git a/bin/run-one-job.ts b/bin/run-one-job.ts index 3da7cf9..cd0919c 100644 --- a/bin/run-one-job.ts +++ b/bin/run-one-job.ts @@ -22,7 +22,8 @@ // 3 = TOKEN_EXPIRED detected → run-agent.sh schrijft TOKEN_EXPIRED marker import { spawn, spawnSync } from 'node:child_process' -import { mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { basename, join } from 'node:path' import { Client as PgClient } from 'pg' @@ -196,6 +197,25 @@ async function main(): Promise { log(`claimed job_id=${jobId}`) + // Per-job log: symlink jobs/.log -> the runs/.log of + // this iteration. runs/ files are timestamp-named, so without this a job's + // output is only findable by grepping. run-agent.sh passes the run-log + // path via RUN_LOG. Relative target so it survives the host bind-mount. + // Best-effort — never fail the job over a log convenience. Dangling links + // (after the runs/ file is gzipped/deleted) are pruned by log-cleanup.sh. + const runLog = process.env.RUN_LOG + if (runLog) { + try { + const jobsDir = join(process.env.AGENT_LOG_DIR ?? '/var/log/agent', 'jobs') + mkdirSync(jobsDir, { recursive: true }) + const linkPath = join(jobsDir, `${jobId}.log`) + rmSync(linkPath, { force: true }) + symlinkSync(join('..', 'runs', basename(runLog)), linkPath) + } catch (err) { + log(`per-job log symlink skipped for ${jobId}: ${(err as Error).message}`) + } + } + // 3. Resolve full context. let ctx: Awaited> = null try { From c64c0278f2396c3908ff57aa8511d72c086f4039 Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Thu, 14 May 2026 19:29:14 +0200 Subject: [PATCH 2/3] feat(worker): configureerbare Claude --output-format, default stream-json (IDEA-064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run-one-job.ts spawnde Claude met een hardcoded --output-format text, dus de run-log bevatte alleen Claude's eind-samenvatting — geen zicht op het werk tijdens een job (~6-10 min stilte, dan ineens de samenvatting). - --output-format komt nu uit AGENT_CLAUDE_OUTPUT_FORMAT (default 'stream-json'). stream-json streamt elke tool-call / elk bericht live naar de run-log; --verbose wordt automatisch toegevoegd want print-mode vereist dat bij stream-json. - Zet AGENT_CLAUDE_OUTPUT_FORMAT=text terug voor de oude terse output. - .env.example: nieuwe var gedocumenteerd. stdoutBuf wordt alleen voor de TOKEN_EXPIRED-regexscan gebruikt; de auth-error-strings staan ook binnen de JSON-events, dus detectie werkt ongewijzigd. Niets parseert de output als job-resultaat. Gevolg: de run-log (en de jobs/.log symlink uit IDEA-063) wordt JSONL i.p.v. plain text — gebruik jq of een viewer. Log-grootte groeit; rotate-logs.sh dekt dat al af. node --check + type-strip schoon. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 6 ++++++ bin/run-one-job.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 3f3a523..0acc9f9 100644 --- a/.env.example +++ b/.env.example @@ -108,3 +108,9 @@ AGENT_BACKOFF_MAX=300 AGENT_LOG_GZIP_AFTER_HOURS=24 # Hoeveel dagen ge-gzipte logs bewaren voor we ze verwijderen. AGENT_LOG_DELETE_AFTER_DAYS=30 + +# Claude CLI --output-format. Default 'stream-json' streamt de volledige +# event-stream (tool-calls, berichten) live naar de run-log; 'text' geeft +# alleen Claude's eind-samenvatting (terser, maar geen live-meekijken). +# stream-json maakt de run-log JSONL — gebruik jq of een viewer. +AGENT_CLAUDE_OUTPUT_FORMAT=stream-json diff --git a/bin/run-one-job.ts b/bin/run-one-job.ts index cd0919c..8155fea 100644 --- a/bin/run-one-job.ts +++ b/bin/run-one-job.ts @@ -292,6 +292,13 @@ async function main(): Promise { // 7. Build CLI args. const promptText = getKindPromptText(ctx.kind).replace('$PAYLOAD_PATH', payloadPath) + // --output-format is configureerbaar via env. Default 'stream-json' geeft + // de volledige event-stream (elke tool-call, elk bericht) live in de + // run-log, i.p.v. alleen Claude's eind-samenvatting. stream-json vereist + // --verbose in print-mode. Zet AGENT_CLAUDE_OUTPUT_FORMAT=text terug voor + // de oude terse output. TOKEN_EXPIRED-detectie werkt ongewijzigd: de + // auth-error-strings staan ook binnen de JSON-events. + const outputFormat = process.env.AGENT_CLAUDE_OUTPUT_FORMAT ?? 'stream-json' const args: string[] = [ '-p', promptText, @@ -306,8 +313,9 @@ async function main(): Promise { '--add-dir', '/opt/agent', '--output-format', - 'text', + outputFormat, ] + if (outputFormat === 'stream-json') args.push('--verbose') if (effort) args.push('--effort', effort) const cwd = worktreePath ?? '/opt/agent' From a051bb00d43e53b8dbee21cc4ed865f855517c6d Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Fri, 15 May 2026 00:43:32 +0200 Subject: [PATCH 3/3] fix(worker): TOKEN_EXPIRED-detectie alleen bij non-zero Claude-exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run-one-job.ts scant de volledige stream-json output (incl. álle tool-results) op auth-error-patronen, en run-agent.sh grept hetzelfde over het complete run-log — beide zonder de exit-code te checken. Daardoor legt een geslaagde job (exit 0, result.is_error=false) de worker plat zodra z'n output toevallig iets als "401 unauthorized" bevat — bv. wanneer de agent een doc over route-handler-auth leest of een endpoint test. run-agent.sh doet dan touch TOKEN_EXPIRED + sleep infinity en de worker draait pas na een rebuild weer. Fix: detectie gaten op een niet-nul exit. Een echte credential-fout laat 'claude' non-zero exiten, dus echte expiries worden nog steeds gevangen — alleen de false positives op geslaagde runs verdwijnen. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/run-agent.sh | 8 ++++++-- bin/run-one-job.ts | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/bin/run-agent.sh b/bin/run-agent.sh index 52c6f49..6b3dd32 100644 --- a/bin/run-agent.sh +++ b/bin/run-agent.sh @@ -80,8 +80,12 @@ while true; do # Token-expiry detectie: run-one-job.ts retourneert exit 3 wanneer het # bekende auth-error-strings in Claude's output ziet. We checken óók de # log-tekst voor het geval een ander pad het patroon raakt (bv. Prisma- - # connection-error met OAuth-expired in error-body). - if [[ "$exit_code" -eq 3 ]] || grep -qE '(invalid_api_key|authentication.*failed|401.*unauthor|OAuth.*expired)' "${run_log}"; then + # connection-error met OAuth-expired in error-body) — maar alléén bij een + # niet-nul exit. Het run-log bevat de volledige stream-json output (incl. + # tool-results én run-one-job's eigen "TOKEN_EXPIRED detected"-logregel), + # dus een geslaagde job die toevallig "401 unauthorized" in z'n output + # heeft mag de grep-fallback niet triggeren. + if [[ "$exit_code" -eq 3 ]] || { [[ "$exit_code" -ne 0 ]] && grep -qE '(invalid_api_key|authentication.*failed|401.*unauthor|OAuth.*expired)' "${run_log}"; }; then log "AUTH FAILURE detected (exit=$exit_code or pattern in log) — marking TOKEN_EXPIRED" touch "${AGENT_STATE_DIR}/TOKEN_EXPIRED" write_state "$(jq -n \ diff --git a/bin/run-one-job.ts b/bin/run-one-job.ts index 8155fea..f9cc879 100644 --- a/bin/run-one-job.ts +++ b/bin/run-one-job.ts @@ -383,13 +383,21 @@ async function main(): Promise { `duration_ms=${durationMs} wall_clock_seconds=${Math.round(durationMs / 1000)}`, ) - // 10. Token-expiry detection. + // 10. Token-expiry detection — alleen als Claude zelf non-zero eindigde. + // stdoutBuf bevat de volledige stream-json output incl. álle tool-results, + // dus de auth-error-strings kunnen ook agent-werk-content zijn (een doc + // over 401-handling gelezen, een endpoint getest). Een echte credential- + // fout laat 'claude' non-zero exiten; een geslaagde run (exit 0) is per + // definitie geen token-expiry. Zonder deze gate legt zulke content de + // worker onterecht plat (run-agent.sh → TOKEN_EXPIRED marker + sleep). let tokenExpired = false - for (const pat of TOKEN_EXPIRY_PATTERNS) { - if (pat.test(stdoutBuf)) { - tokenExpired = true - log(`TOKEN_EXPIRED detected pattern="${pat.source}" exiting code=3`) - break + if (exitCode !== 0) { + for (const pat of TOKEN_EXPIRY_PATTERNS) { + if (pat.test(stdoutBuf)) { + tokenExpired = true + log(`TOKEN_EXPIRED detected pattern="${pat.source}" exiting code=3`) + break + } } }