generated from coulomb/repo-seed
feat(token-events): auto-capture real token counts via PostToolUse hook
- Add PATCH /token-events/{id} endpoint to correct heuristic events
- Add `note` filter to GET /token-events/ list
- Add TokenEventPatch schema
- Add task_token_hook.py: PostToolUse hook that reads the Claude Code
session transcript, computes per-task token delta, and replaces the
heuristic token event with real measured counts (note="measured")
- Register hook in ~/.claude/settings.json on mcp__state-hub__update_task_status
Covers both interactive sessions and ralph-workplan loops
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ from api.models.managed_repo import ManagedRepo
|
||||
from api.models.task import Task
|
||||
from api.models.token_event import TokenEvent
|
||||
from api.models.workstream import Workstream
|
||||
from api.schemas.token_event import RepoTokenSummary, TokenEventCreate, TokenEventRead, TokenSummary
|
||||
from api.schemas.token_event import RepoTokenSummary, TokenEventCreate, TokenEventPatch, TokenEventRead, TokenSummary
|
||||
|
||||
router = APIRouter(prefix="/token-events", tags=["token-events"])
|
||||
|
||||
@@ -166,6 +166,22 @@ async def get_tokens_by_repo(
|
||||
]
|
||||
|
||||
|
||||
@router.patch("/{event_id}", response_model=TokenEventRead)
|
||||
async def patch_token_event(
|
||||
event_id: uuid.UUID,
|
||||
body: TokenEventPatch,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> TokenEvent:
|
||||
event = await session.get(TokenEvent, event_id)
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Token event not found")
|
||||
for field, value in body.model_dump(exclude_none=True).items():
|
||||
setattr(event, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
@router.get("/{event_id}", response_model=TokenEventRead)
|
||||
async def get_token_event(
|
||||
event_id: uuid.UUID,
|
||||
@@ -186,6 +202,7 @@ async def list_token_events(
|
||||
ref_id: str | None = None,
|
||||
model: str | None = None,
|
||||
agent: str | None = None,
|
||||
note: str | None = None,
|
||||
limit: int = Query(100, le=1000),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[TokenEvent]:
|
||||
@@ -204,6 +221,8 @@ async def list_token_events(
|
||||
q = q.where(TokenEvent.model == model)
|
||||
if agent:
|
||||
q = q.where(TokenEvent.agent == agent)
|
||||
if note:
|
||||
q = q.where(TokenEvent.note == note)
|
||||
q = q.order_by(TokenEvent.created_at.desc()).limit(limit)
|
||||
result = await session.execute(q)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@@ -52,6 +52,14 @@ class TokenSummary(BaseModel):
|
||||
by_agent: dict[str, int]
|
||||
|
||||
|
||||
class TokenEventPatch(BaseModel):
|
||||
tokens_in: int | None = None
|
||||
tokens_out: int | None = None
|
||||
note: str | None = None
|
||||
model: str | None = None
|
||||
agent: str | None = None
|
||||
|
||||
|
||||
class RepoTokenSummary(BaseModel):
|
||||
repo_id: uuid.UUID
|
||||
repo_slug: str
|
||||
|
||||
Reference in New Issue
Block a user