Replace internal task tracking (TODO.md, FINDINGS.md) with structured documentation in docs/. Trim CLAUDE.md to agent instructions only. Co-Authored-By: Virgil <virgil@lethean.io>
10 KiB
go-agentic Project History
Module: forge.lthn.ai/core/go-agentic
Origin: Extraction from go-ai
Date: 19 February 2026
Commit: 68c108f feat: extract go-agentic from go-ai as standalone service package
The package was extracted from forge.lthn.ai/core/go-ai/agentic/. The agentic subdirectory in go-ai imported only forge.lthn.ai/core/go, gopkg.in/yaml.v3, and the standard library — no coupling to go-ai/ml, go-ai/rag, go-ai/mcp, or any other subpackage. This made it the cleanest extraction candidate in the go-ai monolith.
What was extracted at the split point:
- 14 Go source files (~1,968 lines, excluding tests)
- 5 test files covering allowance, client, completion, config, and context
- 1 embedded prompt template (
prompts/commit.md)
After extraction, a go.mod replace directive was corrected from ../core to ../go to match the actual sibling directory name (af110be).
Phase 1: Test Coverage
Commit: 23aa635 test: achieve 85.6% coverage with 7 new test files
Operator: Charon
Coverage improved from 70.1% to 85.6% with 7 new test files and over 130 tests:
| New file | Purpose |
|---|---|
lifecycle_test.go |
Full claim -> process -> complete integration; fail/cancel flows; concurrent agents |
allowance_edge_test.go |
Boundary: exact limit, one-over, zero allowance, warning threshold |
allowance_error_test.go |
Mock errorStore to exercise all error paths in RecordUsage/Check/ResetAgent |
embed_test.go |
Prompt() hit/miss and whitespace trimming |
service_test.go |
DefaultServiceOptions, TaskPrompt Set/GetTaskID, TaskCommit fields |
completion_git_test.go |
AutoCommit, CreateBranch, CommitAndSync, GetDiff using real git repositories |
context_git_test.go |
findRelatedCode in git repos: keyword search, 10-file cap, truncation |
Discovery: MemoryStore correctly uses defensive copies on Set/Get. Mutations to a struct after SetAllowance do not affect the stored data.
Discovery: AllowanceService.Check enforces limits in a fixed priority order: model allowlist -> daily tokens -> daily jobs -> concurrent jobs -> global model budget. When multiple limits are exceeded simultaneously, the first in this order is reported.
A follow-up commit (5d02695) pushed coverage further to 96.5%.
Phase 2: Allowance Persistence
Commit: 3e43233 feat: Phase 2 — SQLite AllowanceStore backend + config wiring
Commit: 0be744e feat(allowance): add Redis backend for AllowanceStore
MemoryStore lost all state on process restart. Two persistent backends were added:
SQLiteStore— single-node persistence viaforge.lthn.ai/core/go-store(SQLite KV). Read-modify-write operations are serialised withsync.Mutex.time.Durationis stored as int64 nanoseconds to avoid locale-dependent string parsing.RedisStore— multi-process persistence viagithub.com/redis/go-redis/v9. Atomic increment/decrement operations use Lua scripts (EVAL) to avoid TOCTOU races.
AllowanceConfig and NewAllowanceStoreFromConfig were added to config.go as the backend selection factory.
Phase 3: Multi-Agent Coordination
Commit: 646cc02 feat(coordination): add agent registry, task router, and dispatcher
Three new files introduced the multi-agent layer:
registry.go—AgentInfostruct,AgentRegistryinterface,MemoryRegistryimplementation.Reap(ttl)marks stale agents offline and returns their IDs.router.go—TaskRouterinterface,DefaultRouterimplementation. Capability matching (task labels must be a subset of agent capabilities), load-based scoring (1 - load/max), least-loaded selection for critical tasks.ErrNoEligibleAgentsentinel.dispatcher.go—Dispatchercombining registry, router, allowance service, and API client.Dispatchexecutes the five-step pipeline.DispatchLooppolls for pending tasks on a ticker.
Phase 4: CLI Backing Functions
Commit: ef81db7 feat(cli): add status summary, task submission, and log streaming
Three files added to serve the core agent CLI commands (implemented separately in core/cli):
status.go—StatusSummary,GetStatus,FormatStatus. Aggregates registry, task counts, and allowance remaining. All components are optional (nil-safe).submit.go—SubmitTask. Validates title, setsStatusPendingandCreatedAt, delegates toclient.CreateTask.logs.go—StreamLogs. PollsGetTaskat an interval, writes timestamped status lines to anio.Writer, stops on terminal states.client.gogainedCreateTask(POST /api/tasks).
Phase 5: Persistent Agent Registry
Commit: ce502c0 feat(registry): Phase 5 — persistent agent registry (SQLite + Redis + config factory)
MemoryRegistry lost all agent registrations on restart. The same persistence pattern from Phase 2 was applied to the registry:
registry_sqlite.go—SQLiteRegistryusingdatabase/sqlwithmodernc.org/sqlitedirectly. Schema:agentstable with UPSERT on Register, WAL mode,busy_timeout=5000ms.registry_redis.go—RedisRegistrywith TTL-based natural expiry serving as the reap mechanism. SCAN-basedReapas a backup.RegistryConfigandNewAgentRegistryFromConfigadded toconfig.go.
Phase 6: Dead Code Cleanup
Commit: 779132a fix(config): change DefaultBaseURL to localhost, annotate reserved fields
Two issues addressed:
DefaultBaseURLwasapi.core-agentic.dev(a non-existent host). Changed tohttp://localhost:8080. Production deployments must setAGENTIC_BASE_URL.HourlyRateLimitandCostCeilingonModelQuotawere stored but never enforced inAllowanceService.Check. Enforcement would requireAllowanceStore.GetHourlyUsage(a sliding window query), which would be a breaking interface change. The fields are retained and annotated as reserved. All three backends correctly store and round-trip both values.
Phase 7: Priority-Ordered Dispatch and Retry
Commit: ba8c19d feat(dispatch): Phase 7 — priority-ordered dispatch with retry backoff and dead-letter
DispatchLoop previously dispatched tasks in arbitrary API order with no retry handling. Two improvements:
- Priority sorting: tasks are sorted by
priorityRank(Critical=0, High=1, Medium=2, Low=3) then byCreatedAtascending (oldest first for tasks of equal priority).sort.SliceStableis used for determinism. - Exponential backoff and dead-letter: tasks with
RetryCount > 0are skipped untilLastAttempt + backoffDuration(RetryCount) > now. Backoff starts at 5 seconds and doubles per retry (5s, 10s, 20s, ...). Tasks reachingMaxRetries(default 3) are updated toStatusFailedviaclient.UpdateTaskand the failure reason is set to"max retries exceeded".MaxRetriesandRetryCountfields were added to theTasktype.
Phase 8: Event Hooks
Commit: a29ded5 feat(events): Phase 8 — event hooks for task lifecycle and quota notifications
Production orchestration required external notification of lifecycle transitions. Three new constructs:
events.go—Eventstruct,EventType(8 constants),EventEmitterinterface,ChannelEmitter(buffered channel, drops on overflow),MultiEmitter(fan-out, failure-tolerant).Dispatcher.SetEventEmitterandemithelper — emitstask_dispatched,task_claimed,dispatch_failed_no_agent,dispatch_failed_quota,task_dead_lettered.AllowanceService.SetEventEmitterandemitEventhelper — emitsquota_warning(at 80% of daily token limit),quota_exceeded(five distinct check paths),usage_recorded(on job started and completed).
Both SetEventEmitter callers are nil-safe: emission is always a no-op when no emitter is set.
12 integration tests verify all emission points in events_integration_test.go.
Known Limitations
The following limitations were documented during development and have not yet been addressed.
service.go Coverage Gap
NewService, OnStartup, handleTask, doCommit, and doPrompt are at 0% test coverage. These functions require a full framework.Core DI container and spawn a claude subprocess. A mock subprocess approach (exec.Command with a test binary) is possible but was deferred to a later phase. A minimal mock binary approach was explored in service_test.go (commit 9636cdb) for HandleTask tests.
completion.go CreatePR
CreatePR calls gh pr create as a subprocess. It is at 14.3% coverage. Testing requires gh installed and authenticated against a real or stub GitHub API. Not suitable for unit tests.
HourlyRateLimit and CostCeiling Not Enforced
Both fields on ModelQuota are stored correctly by all three backends but are never checked in AllowanceService.Check. Enforcement would require a new AllowanceStore.GetHourlyUsage(agentID string, since time.Time) (int64, error) method — a breaking interface change that would require updates to all three implementations plus their tests. This was deferred indefinitely.
DispatchLoop Task State Not Re-fetched
After a failed dispatch that increments RetryCount and sets LastAttempt, the loop modifies the local copy of the task in the tasks slice. It does not call client.UpdateTask to persist RetryCount/LastAttempt. If the process restarts, those values are lost and the task will be dispatched again from a zero retry count. Fixing this requires either persisting retry state via the API or a separate task state store.
No Heartbeat Loop
AgentRegistry.Heartbeat and Reap exist but there is no built-in background goroutine to call them. Callers must schedule heartbeats and reaping externally.
Future Considerations
Items identified during development but not yet scoped:
- Interface extraction for
service.go: Extract the subprocess invocation into aClaudeRunnerinterface to enable unit testing without a realclaudebinary. AllowanceStore.GetHourlyUsage: Required to enforceHourlyRateLimit. Would enable per-hour rate limiting across all three backends.- Persist retry state via API:
DispatchLoopshould callclient.UpdateTaskto persistRetryCountandLastAttemptso that retries survive process restarts. - Built-in heartbeat loop: A
StartHeartbeat(ctx, registry, agentID, interval)helper to callHeartbeaton a ticker andReapon a longer interval. - WebSocket event emitter: A
WebSocketEmitterbacked byforge.lthn.ai/core/go/pkg/wsfor real-time event streaming to external consumers. - Allowance daily reset scheduler: A background goroutine that calls
AllowanceService.ResetAgentat midnight UTC for each registered agent.