Files
comun/utile/context_watch.ps1
Marius Mutu 4898f8bd68 context_watch: masoara si contextul fiecarui subagent
Pe SubagentStop hook-ul primeste agent_transcript_path - transcriptul
separat al subagentului care tocmai s-a oprit. -Subagent il citeste pe
acela in loc de transcriptul sesiunii, cu praguri mai joase (150k/200k),
si spune orchestratorului sa nu mai dea sarcini agentului respectiv.
Acopera gaura cunoscuta: subagentii nu-si simt singuri pragul.

-Json ambaleaza mesajul in hookSpecificOutput/additionalContext.
Garda stop_hook_active taie a doua declansare a aceluiasi ciclu de stop,
care producea reinjectarea in bucla.

Probat pe input real de hook capturat: 40k citit din transcriptul unui
subagent, tacere sub prag, tacere la stop_hook_active=true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZwtBDjGbAWvy7TmcNVxDC
2026-09-17 22:26:50 +03:00

146 lines
7.3 KiB
PowerShell

# context_watch.ps1 - cat context ocupa sesiunea curenta Claude Code.
# Citeste ultimul "usage" din transcriptul JSONL al sesiunii (input + cache_read + cache_creation).
# Utilizare:
# - ca hook (PostToolUse): primeste pe stdin JSON-ul hook-ului, cu transcript_path;
# - manual: powershell -File context_watch.ps1 [-TranscriptPath <cale.jsonl>] [-Prag 250000]
# Iese cu 2 si scrie pe stderr cand contextul depaseste pragul (asa ajunge mesajul la agent).
# -OSinguraData: emite o singura data per prag per sesiune (marcaj in %TEMP%). Necesar pe PostToolUse,
# altfel mesajul s-ar repeta la fiecare apel de tool si ar deveni zgomot ignorat.
# -StareFile <cale>: la pragul de avertisment/max, garanteaza ca fisierul exista si are un antet
# datat pe primul rand ("<!-- context_watch: DD.MM.YYYY HH:mm | tokeni: N | nivel: ... -->"),
# inlocuit la fiecare rulare, nu acumulat. Restul continutului (scris de model) ramane neatins.
[CmdletBinding()]
param(
[string]$TranscriptPath,
[int]$Prag = 250000,
[int]$PragMax = 275000,
[switch]$Intotdeauna,
[switch]$Stdout, # UserPromptSubmit: mesajul se scrie pe stdout si se iese cu 0
[switch]$OSinguraData, # PostToolUse: o singura alerta per prag atins, nu la fiecare tool
[string]$StareFile, # optional: fisier de stare cu antet datat, actualizat la prag atins
[switch]$Subagent, # SubagentStop: masoara agent_transcript_path, nu transcriptul sesiunii
[switch]$Json # emite {"hookSpecificOutput":{...,"additionalContext":...}} pe stdout
)
# subagentii nu-si simt pragul singuri: se taie sarcina mai devreme decat la sesiunea principala
if ($Subagent) {
if (-not $PSBoundParameters.ContainsKey('Prag')) { $Prag = 150000 }
if (-not $PSBoundParameters.ContainsKey('PragMax')) { $PragMax = 200000 }
}
function Scrie-AntetStare([string]$cale, [int]$tokeni, [string]$nivel) {
$antet = "<!-- context_watch: $(Get-Date -Format 'dd.MM.yyyy HH:mm') | tokeni: $tokeni | nivel: $nivel -->"
if (Test-Path $cale) {
$continut = Get-Content -Path $cale -Encoding UTF8
if ($continut.Count -gt 0 -and $continut[0] -match '^<!-- context_watch:') {
$continut[0] = $antet
} else {
$continut = ,$antet + $continut
}
} else {
$continut = ,$antet
}
Set-Content -Path $cale -Value $continut -Encoding UTF8
}
$agentEtich = ''
if (-not $TranscriptPath) {
$stdin = [Console]::In.ReadToEnd()
if ($stdin) {
try {
$h = ConvertFrom-Json $stdin
# a doua declansare a aceluiasi ciclu de stop: taci, altfel hook-ul se reinjecteaza in bucla
if ($h.stop_hook_active -eq $true) { exit 0 }
# pe SubagentStop hook-ul primeste si transcriptul separat al subagentului
if ($Subagent -and $h.agent_transcript_path) {
$TranscriptPath = $h.agent_transcript_path
$agentEtich = $(if ($h.agent_type) { $h.agent_type } else { $h.agent_id })
} else {
$TranscriptPath = $h.transcript_path
}
} catch { }
}
}
# fara transcript de subagent nu avem ce masura; nu cadem pe sesiunea principala
if ($Subagent -and -not $TranscriptPath) { exit 0 }
# la primul mesaj al unei sesiuni noi transcriptul inca nu e creat: fara fallback, ca sa nu raporteze alta sesiune
if ($TranscriptPath -and -not (Test-Path $TranscriptPath)) { exit 0 }
if (-not $TranscriptPath) {
$dir = Join-Path $env:USERPROFILE ".claude\projects\$((Get-Location).Path -replace '[:\\/]','-')"
if (Test-Path $dir) {
$f = Get-ChildItem "$dir\*.jsonl" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($f) { $TranscriptPath = $f.FullName }
}
}
if (-not $TranscriptPath -or -not (Test-Path $TranscriptPath)) { exit 0 }
# doar coada fisierului - transcriptul poate avea zeci de MB
$fs = [IO.File]::Open($TranscriptPath, 'Open', 'Read', 'ReadWrite')
try {
$len = [Math]::Min(400KB, $fs.Length)
$fs.Seek(-$len, 'End') | Out-Null
$buf = New-Object byte[] $len
$fs.Read($buf, 0, $len) | Out-Null
} finally { $fs.Close() }
$coada = [Text.Encoding]::UTF8.GetString($buf)
$m = [regex]::Matches($coada, '"usage":\{.*?"output_tokens":\d+')
if ($m.Count -eq 0) { exit 0 }
$ultim = $m[$m.Count - 1].Value
$total = 0
foreach ($camp in @('input_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens')) {
if ($ultim -match ('"' + $camp + '":(\d+)')) { $total += [int]$Matches[1] }
}
$k = [Math]::Round($total / 1000)
$mesaj = ''
$nivel = ''
if ($Subagent) {
$cine = $(if ($agentEtich) { "Subagentul '$agentEtich'" } else { 'Subagentul care tocmai s-a oprit' })
if ($total -ge $PragMax) {
$nivel = 'max'
$mesaj = "$cine a consumat ${k}k context - PESTE limita de $([Math]::Round($PragMax/1000))k. " +
"NU-i mai da nicio sarcina. Cere-i handoff pe disc daca a ramas ceva neconsemnat, " +
"apoi opreste-l si spawneaza unul proaspat pentru blocul urmator."
} elseif ($total -ge $Prag) {
$nivel = 'avertisment'
$mesaj = "$cine a consumat ${k}k context. Sarcina urmatoare se taie sau se da unui agent proaspat - " +
"subagentii nu-si simt singuri pragul. Verifica intai ca si-a scris livrabilul pe disc."
}
} elseif ($total -ge $PragMax) {
$nivel = 'max'
$mesaj = "CONTEXT ${k}k - PESTE LIMITA de $([Math]::Round($PragMax/1000))k. REGULA ZERO, predare OBLIGATORIE ACUM: " +
"(1) opreste lucrul, nu incepe nimic nou, adu editarile in curs la o stare consistenta pe disc; " +
"(2) scrie handoff-ul PE DISC in docs\handoff_<subiect>.md (nu doar in mesaj) - inventar livrabile cu fisier:linie, " +
"ce e terminat si ce nu, DACA write-back-ul e facut per fisier, comanda de rulare a testelor, ce s-a stabilit deja, " +
"ce e interzis, starea datelor de test, capcanele de mediu; " +
"(3) confirma in doua randuri ce ramane si ce e intr-o stare periculoasa; (4) preda unei sesiuni noi. " +
"'Mai am putin' nu e motiv de amanare."
} elseif ($total -ge $Prag) {
$nivel = 'avertisment'
$mesaj = "CONTEXT ${k}k - peste $([Math]::Round($Prag/1000))k. REGULA ZERO: incheie blocul curent, actualizeaza handoff-ul pe disc " +
"(docs\handoff_<subiect>.md / docs\progres.md) si pregateste predarea. Deleaga la subagenti tot ce mai ai de citit sau de testat - " +
"nu citi loguri brute si nu diagnostica prin citire de cod in sesiunea asta."
}
if ($mesaj) {
if ($StareFile) { Scrie-AntetStare -cale $StareFile -tokeni $total -nivel $nivel }
if ($OSinguraData -and $nivel) {
# marcaj per sesiune+prag: transcriptul identifica sesiunea, deci si numele fisierului de marcaj
$cheie = [IO.Path]::GetFileNameWithoutExtension($TranscriptPath) + '_' + $nivel
$marcaj = Join-Path $env:TEMP "claude_ctxwatch_$cheie.flag"
if (Test-Path $marcaj) { exit 0 }
New-Item -ItemType File -Path $marcaj -Force | Out-Null
}
if ($Json) {
$ev = $(if ($Subagent) { 'SubagentStop' } else { 'PostToolUse' })
$o = @{ hookSpecificOutput = @{ hookEventName = $ev; additionalContext = $mesaj } }
Write-Output ($o | ConvertTo-Json -Compress -Depth 4)
exit 0
}
if ($Stdout) { Write-Output $mesaj; exit 0 }
[Console]::Error.WriteLine($mesaj)
exit 2
}
if ($Intotdeauna) { Write-Output "CONTEXT ${k}k" }
exit 0