Skip to content

feat(usage): tool usage logging for auto-compact#224

Merged
birdmanmandbir merged 3 commits intomainfrom
worker/auto-compact-programmatic-fast
Mar 12, 2026
Merged

feat(usage): tool usage logging for auto-compact#224
birdmanmandbir merged 3 commits intomainfrom
worker/auto-compact-programmatic-fast

Conversation

@birdmanmandbir
Copy link
Contributor

Summary

  • Adds ToolUsage ent schema (internal/ent/schema/tool_usage.go) with fields: agent, team, command, subcommand, target, created_at
  • Adds internal/usage package with fire-and-forget Log() and Summary() query helper
  • Instruments 5 commands: explore, task route, task execute, send, task add

Logging skips silently when TTAL_AGENT_NAME is unset (human context). The Summary() function groups by subcommand+target and returns counts, ready for the auto-compact session summary feature.

Test plan

  • Set TTAL_AGENT_NAME=test-agent and run ttal explore "test" --web → verify row in tool_usages table via sqlite3 ~/.ttal/messages.db "SELECT * FROM tool_usages"
  • Run without TTAL_AGENT_NAME → verify no row, no error
  • Run ttal task add, ttal send, ttal task route, ttal task execute → verify rows logged per command
  • Verify existing command behavior unchanged

All subagents need bash to run git diff, flicknote get, ls, etc.
The ttal tools config was missing it while claude-code config had it.
Adds a new ToolUsage ent schema and internal/usage package that logs
ttal command invocations to SQLite (messages.db). Logging is fire-and-
forget and skips silently when TTAL_AGENT_NAME is unset (human context).

Commands instrumented: explore, task route, task execute, send, task add.
Includes Summary() query helper grouped by subcommand+target for use
in the upcoming auto-compact feature.
@birdmanmandbir
Copy link
Contributor Author

PR Review: feat(usage): add tool usage logging for auto-compact session summaries

Critical Issues

None.

Important Issues

1. [log.go:57-68] Driver double-wrap creates a resource leak on Schema.Create failure (confidence: 87)

entsql.Open("sqlite", dbDSN) creates drv wrapping a *sql.DB. Then entsql.OpenDB("sqlite3", drv.DB()) creates a second driver wrapping the same *sql.DB for the ent client. When client.Schema.Create fails, client.Close() is called — but drv is never closed, leaking the underlying connection. Simplify to pass drv directly:

drv, err := entsql.Open("sqlite", dbDSN)
if err != nil {
    return nil, err
}
client := ent.NewClient(ent.Driver(drv))

2. [log.go:65] Schema.Create uses context.Background() (no timeout) — can block CLI indefinitely (confidence: 85 / HIGH)

Log() is called inline in every command handler and is supposed to be non-blocking. But open() runs client.Schema.Create(context.Background()) before the 2-second timeout context is even created. If the DB is locked (daemon mid-write), this call blocks forever — violating the fire-and-forget contract and hanging the user's CLI.

Fix: pass the bounded context into open():

func open(ctx context.Context) (*ent.Client, error) { ... }
// then: client.Schema.Create(ctx)

3. [log.go:65] Schema.Create runs the full schema migration on every open() call (confidence: 85)

This migrates all ent tables — including attachments, messages, reactions — that the usage package doesn't own. It runs on every single ttal command invocation (~5–15ms per call, no timeout). Either scope the migration to ToolUsage only, or remove Schema.Create from open() and rely on the daemon to own schema init.

4. [log.go:27-30] Silent open() failure suppresses all diagnostics (MEDIUM)

Write failures are logged (log.Printf("[usage] log error: %v", err)) but open failures are silently dropped — including the case where ~/.ttal/ doesn't exist yet (fresh install before daemon has run). Add a log line to match write-error handling:

client, err := open()
if err != nil {
    log.Printf("[usage] open error: %v", err)
    return
}

Suggestions

  • [task.go, task_route.go] usage.Log is called before the underlying operation succeeds (before priority validation, before worker.Spawn). This is fine if the intent is "command attempted" semantics, but should be documented in the Log() godoc so future call sites are consistent.
  • [query.go] Summary() has no callers yet in this PR. When the consumer is written, document that (nil, err) is expected when ~/.ttal/ doesn't exist, so callers treat it as non-fatal.

Strengths

  • Fire-and-forget design is correct for telemetry — Log() never propagates errors to callers
  • Guard clause on empty TTAL_AGENT_NAME cleanly short-circuits before any DB work
  • doc.go with plane annotation (// Plane: shared) present and correct
  • open() correctly closes client on Schema.Create failure (no happy-path leak)
  • Indexes on (agent, created_at) and (agent, subcommand, created_at) cover Summary() query access patterns

VERDICT: NEEDS_WORK

The unbounded Schema.Create context (issue #2) is the blocker — it can hang the CLI if the DB is locked, which is likely during normal daemon operation. Issues #1 (driver leak) and #3 (full schema migration scope) should be fixed alongside it. Issue #4 is a minor diagnostic improvement.

…pen error log

- Replace entsql.OpenDB double-wrap with direct ent.NewClient(ent.Driver(drv))
  to eliminate driver resource leak on Schema.Create failure
- Remove Schema.Create from open() — daemon owns schema init; CLI logging
  is fire-and-forget and silently skips if tables don't exist yet
- Add log.Printf on open() failure to match write-error diagnostic level
- Document "command attempted" semantics in Log() godoc
- Document non-fatal (nil, err) contract on Summary() for unavailable DB
@birdmanmandbir
Copy link
Contributor Author

Triage Update

Fixed

  • Driver double-wrap resource leak — replaced entsql.OpenDB(drv.DB()) with ent.NewClient(ent.Driver(drv)) directly; drv no longer leaks on failure (ec7ddf7)
  • Unbounded Schema.Create context hangs CLI — removed Schema.Create from open() entirely (see below) (ec7ddf7)
  • Schema.Create runs full migration on every call — daemon owns schema init; CLI open() no longer migrates. Fire-and-forget log calls silently skip if tables don't exist (daemon not yet started) (ec7ddf7)
  • Silent open() failure — added log.Printf("[usage] open error: %v", err) to match write-error diagnostic level (ec7ddf7)
  • "Command attempted" semantics — documented in Log() godoc (ec7ddf7)
  • Summary() non-fatal contract — documented (nil, err) behavior when DB unavailable (ec7ddf7)

@birdmanmandbir
Copy link
Contributor Author

Re-review: fix(usage): remove double-wrap, drop Schema.Create from open(), add open error log

Fixed

  • Driver double-wrap resource leakopen() now returns ent.NewClient(ent.Driver(drv)) directly; no orphaned driver on failure path
  • Unbounded Schema.Create context / CLI hang riskSchema.Create removed from open() entirely; daemon owns schema init
  • Full schema migration on every callopen() is now a pure connection open, no migration
  • Silent open() failurelog.Printf("[usage] open error: %v", err) added, consistent with write-error logging
  • "Command attempted" semantics — documented in Log() godoc
  • Summary() non-fatal contract — documented (nil, err) behavior when DB unavailable

Remaining

None.

VERDICT: LGTM

@birdmanmandbir birdmanmandbir merged commit 78cf20d into main Mar 12, 2026
1 check passed
@birdmanmandbir birdmanmandbir deleted the worker/auto-compact-programmatic-fast branch March 12, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant