Skip to content
hango: multi-tenant agent harness in Go

hango: multi-tenant agent harness in Go

Overview

hango is a multi-tenant agent harness server written in Go from scratch: API-key auth, durable runs, SSE streaming, middleware-composed harness features (planning, virtual filesystem, subagents, summarization, memory, skills, human-in-the-loop). Built feature by feature as a learning roadmap whose final state is production-ready. Every phase leaves a binary that builds, vets and tests green, at least one integration test against real Postgres where it touches the store, and a learning doc at docs/learning/NN-<slug>.md comparing hango’s choice against goclaw and langchain deepagents.

Locked decisions

  • Go 1.27, standard-library net/http (http.ServeMux plus a small router in internal/server, no web framework), SSE only (no WebSocket). Package cap is 20 plus internal/tool/builtin, enumerated exactly: cmd/hango, migrations, and internal/{app,config,server,auth,store,store/pg,provider,provider/anthropic,provider/openai,agent,tool,tool/builtin,run,fs,usage,telemetry,safego,telegram,claudecli}. scripts/ is excluded because it holds no importable code. Adding a twenty-first package requires an amendment to this line before the code is written.
  • Ids are UUID v7 from the Go 1.27 stdlib uuid package (uuid.NewV7); uuid.Nil is a function and is always called, never compared as a value.
  • Dependency direction is one-way: internal/agent imports nothing from internal/run. agent defines EventSink and Gate; run implements them and the composition root wires them. Any design needing the reverse direction is wrong.
  • Run execution is owned, leased and resumable. A worker starts a run only through a conditional claim (UPDATE ... WHERE state='queued' RETURNING) that sets owner_id and lease_expires_at; every state transition is conditional on state and owner; reconciliation reclaims only expired leases and requeues at a turn boundary. Restart and drain requeue, they do not fail.
  • pause_turn is unreachable and therefore an error. hango registers no Anthropic server tools, so pause_turn surfaces ErrUnsupportedStop. There is no resume counter anywhere.
  • anthropic-sdk-go v1.74 behind hango’s own provider.Provider; normalized Message/Block/Event model designed so an OpenAI adapter synthesizes block start/stop.
  • PostgreSQL only via pgx v5, hand-written SQL, goose embedded migrations. No SQLite, no sqlc, no ORM. Messages are append-only rows (session_id, seq, role, jsonb blocks).
  • Logical tenancy: tenant_id uuid NOT NULL on every tenant-owned table, tenant-leading index, scoped store built from context that fails closed, cross-tenant read test per table from the first table onward. RLS is a late backstop, never a replacement — and enabling it in phase 16 is an explicit refactor onto WithTenantTx plus a small BYPASSRLS system pool for the five paths that precede a tenant scope.
  • Intra-tenant visibility: a member sees and mutates only what they created; owner and admin see and mutate everything in their tenant. Enforced by a per-route authorization matrix plus a shared visibility(scope) store predicate, tested route by route and table by table.
  • hango owns identity: tenants, tenant-scoped users (owner/admin/member), api_keys owned by a user (SHA-256 hash + lookup prefix). No passwords, no login UI, bootstrap admin key from config.
  • Runs are durable rows executed by an in-process scheduler lane worker; SSE subscribes to the persisted event log with Last-Event-ID resume; cancel endpoint; graceful drain. Single replica in v1 (in-process rate limiter and run registry, documented).
  • Middleware architecture: one ordered, data-driven slice with BeforeRun, WrapModelCall, WrapToolCall, ToolProvider, PromptContributor. Every harness feature after phase 7 is a middleware. The system prompt is assembled inside the chain at model-call time as []provider.Block from the contributors in stack order, which is what makes cache breakpoints and memory ordering expressible as block indices. Middleware state is map[string]json.RawMessage marshalled by its owner, so it survives persistence and resume. Tool returns Result{ForLLM, ForUser, IsError}; tool errors become tool results, never panics into the loop; repeat-call detection. No authored base system prompt: policy lives in tool descriptions, the system prompt stays short and dynamic.
  • Audit everything: every mutating API request and every approval decision writes an append-only `audit_events` row carrying request, trace and span ids; audit rows, run events, spans and logs join on `trace_id`.
  • Observability stack: OTel in-process (one TracerProvider/MeterProvider/LoggerProvider, otelslog as the default slog handler) → one OTLP egress to an OpenTelemetry Collector → fan-out to Grafana LGTM (Tempo/Loki/Prometheus) and Langfuse Cloud; GenAI and langfuse.* attribute names are hango-local constants, prompt bodies off by default and stripped in the collector. Observability is a foundation requirement: it ships in phase 1 and every phase instruments itself so any failure is locatable tenant → agent → session → run → turn → model call → tool call.

Goals

#GoalPriority
1Production-ready multi-tenant harness server, single binary, docker compose up; cross-tenant access impossible by construction, proven per tableP1
2Durable, cancellable, resumable runs with SSE streaming; provider abstraction proven by a second (OpenAI-compatible) adapterP1
3Harness features composed as middleware: planning, filesystem, subagents, context management, memory, skills, HITLP1
4Every layer observable and every failure locatable in Grafana and Langfuse; usage accounting and quotasP1
5A learning doc per phase: concept, hango vs goclaw vs deepagents, how to debug that layerP1

Phases

Effort is stated as implementation + learning doc, because the learning doc is a deliverable of every phase, not an afterthought.

#PhaseImplDocEffortDepends on
1Foundation, server skeleton, observability spine15h2h17h—
2Persistence and tenancy core16h2h18h1
3Provider abstraction and Anthropic adapter12h3h15h1
4Agents, sessions, messages10h2h12h2
5Tools, agent loop, durable runs17h3h20h3, 4
6Streaming, cancellation, drain14h2h16h5
7Middleware chain and planning tool11h2h13h6
8Filesystem backend and large-result offload13h2h15h7
9Subagents10h2h12h8
10Context management and summarization11h2h13h8
11Long-term memory and skills7h2h9h9, 10
12OpenAI-compatible provider and provider registry8h2h10h5, 8, 10
13Usage, quotas, rate limits9h2h11h9, 10, 12
14Dashboards, alerts, debugging runbook10h2h12h13, 18, 19
15Human-in-the-loop12h2h14h9, 14
16Production hardening and RLS backstop13h3h16h15
17Evaluation loop8h2h10h16
18Telegram channel15h2h17h6, 13
19Claude Code CLI runtime for the owner12h2h14h6, 8, 13
20Telegram media, pairing and approvals23h3h26h15, 16, 18
Total246h44h290h

Build order

Build order: 1 → 3 → 2 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 18 → 19 → 14 → 15 → 16 → 20 → 17.

Phase 3 depends only on phase 1, so it runs before phase 2. A real model then streams into Tempo and Langfuse at about 31h instead of 49h, and the observability spine has something meaningful to show early. Phase numbers, scope and dependencies do not change. Phases 18 and 19 run right after phase 13, so the family can use hango through Telegram before the dashboard, approval and hardening work; neither adds a migration, so migrations 00001–00011 keep their numbers. Phase 20 runs after phase 16, so its migration 00012_telegram.sql follows the RLS migration and creates its tables with row-level security from the start, and phase 17’s evals can include an image task. build_plan_html.go reads this line, and the page’s “Next up” follows it.

Phase 5 stops twice for review: 5a (harness core: tools and the loop against provider.Fake) and 5b (durability: runs, claim, lease, reclaim, worker, endpoints). Its phase file lists which steps and criteria belong to each checkpoint.

Acceptance criteria

  • Server boots from config, graceful shutdown, health endpoints.
  • Tenant resolved from API key; every store and tool call scoped to the tenant. Cross-tenant access impossible by construction, covered by tests.
  • Users within a tenant can create and run agents.
  • Agent loop with tool calling and streaming over HTTP.
  • Persisted sessions and messages.
  • Subagents, todo/planning tool, per-tenant virtual filesystem, long-term memory.
  • OpenTelemetry traces and structured logs, correlated in Grafana and Langfuse.
  • go test ./... green, go vet clean.
  • docker compose up brings up server and database.
  • hango eval run scores a fixed task set through real runs, pushes the scores to Langfuse, and hango eval compare shows whether a change helped.

Learning docs

Each phase writes docs/learning/NN-<slug>.md (concept, design choices, trade-offs, how goclaw and deepagents do the same thing, why hango diverges, how to debug that layer) and updates docs/learning/README.md and docs/system-architecture.md. Every doc under docs/ starts with front matter holding title and weight (a learning doc’s weight is its phase number) so the Hugo site in site/ orders it; see docs/docs-site.md.

Follow-ups out of scope

  • Bring-your-own provider credentials per tenant (deferred from phase 12). Requirements that work must satisfy: a provider_credentials table with AES-GCM ciphertext whose AAD binds (tenant_id, provider) so a row cannot be replayed into another tenant; a key id per row with primary/secondary keys for rotation; resolution through the existing CredentialLookup seam with no in-memory caching of decrypted secrets; an SSRF allowlist plus post-resolution private-IP rejection for tenant-supplied base_url; metadata-only responses; owner/admin only. Operator-set server credentials remain the default path.
  • Anthropic server tools and opaque raw-block passthrough, which together are what would make pause_turn reachable and resumable. Until then pause_turn is ErrUnsupportedStop.
  • LLM gateway with per-user quotas. One place that owns provider credentials, routes each call to a provider, and enforces quotas per user as well as per tenant, so family members cannot use up each other’s budget and more providers can be added without touching the agent loop. It builds on the phase-12 registry and the phase-13 usage records. Until then the family shares one tenant quota.
  • MCP bridge (MCP servers exposed as tool.Tool implementations); multi-replica deployment (shared rate limiter, event fan-out — note the run ownership lease is already multi-replica-safe); sandboxed execution tool (Sandbox backend with Execute); parallel tool execution within a batch; chat channels other than Telegram, Telegram group chats (goclaw design kept in phase 20, with a configurable history limit), Telegram webhook mode and multi-bot, web chat UI (planned after Telegram) and an admin web page, both in Svelte 5 and talking to the same HTTP API, desktop app, i18n, 20+ providers.

Red Team Review

Four adversarial reviews (security adversary, failure-mode analyst, assumption destroyer, scope and complexity critic) produced 41 findings against the 16 phase files, grouped below into 15 themes plus four decisions the user made directly. Every group was accepted. Reports: plans/reports/redteam-260922-0126-{security-adversary,failure-mode-analyst,assumption-destroyer,scope-complexity-critic}.md.

#FindingSeverityDispositionApplied To
D1BYO per-tenant provider credentials (table, AES-GCM, rotation, 3 endpoints) is scope beyond the contract and an unbounded security surfaceHighAccept (user decision)phase 12 (scope cut to a CredentialLookup seam over operator config, base URL validated https-or-loopback), phase 16 (RLS table list, deployment guide), plan.md follow-ups
D2No intra-tenant authorization model: a member can read and destroy every other user’s dataCriticalAccept (user decision)phase 2 (route matrix, intra-tenant test cases), phase 4 (sessions.created_by, agent/session/message matrix, 404 not 403), phase 5 and 6 (runs and events), phase 8 (files API), phase 15 (approvals)
D3Restart or drain while running loses work or executes a run twice; no ownership leaseCriticalAccept (user decision)phase 5 (owner_id, lease_expires_at, conditional claim, heartbeat, resume_turn, reclaim sweep, tool idempotency), phase 6 (drain requeues, HANGO_DRAIN_TIMEOUT ≥ one turn), phase 16 (deployment guide)
D4pause_turn cannot round-trip hango’s block model, and hango registers no server tools, so the resume path is unreachable codeCriticalAccept (user decision)phase 3 (ErrUnsupportedStop), phase 5 (MaxResumes deleted, fixtures and criteria updated), plan.md locked decisions and follow-ups
F1RLS as designed denies every plain-pool query, starting with API-key authenticationCriticalAcceptphase 16 (system role with BYPASSRLS on a second small pool with five enumerated callers, WithTenantTx refactor of every tenant-scoped call site, never across an LLM call, full suite green under forced RLS, auth path exercised post-RLS)
F2No atomic run claim and non-transactional approval/expiry transitions: runs can execute twiceCriticalAcceptphase 5 (conditional claim, conditional transitions, enqueue dedup, two-worker race test), phase 15 (decision and expiry each transition in their own transaction; two concurrent final approvals ⇒ one execution)
F3On approval resume, RepairToolCalls destroys the batch it is meant to resume; creator self-approval; unvalidated edit argumentsCriticalAcceptphase 15 (decisions loaded before BeforeRun, repair skips decided ids, resume re-drives the pending batch through the tool chain, exactly-once criterion, owner/admin-only and never the creator, edit validated against Parameters() and the when matcher, approval.decided published by the handler)
F4Subagent lane semaphore is acquired re-entrantly: self-deadlock at depth ≥ 2CriticalAcceptphase 9 (non-blocking TryAcquire at depth > 0 with a capacity-refusal tool result, sequential tool batches in v1, parallel-task criterion and test deleted, depth cap kept)
F5internal/agent and internal/run form an import cycle; Agent.Run changes signature in phase 9 with its callers unlistedCriticalAcceptplan.md locked decisions (one-way rule), phase 5 (RunOptions, final signature, call sites enumerated, EventSink owned by agent), phase 9 (injected agent.Gate, no signature change)
F6The load-bearing “Memory after PromptCache” ordering cannot exist: prompt fragments are strings assembled outside the chainCriticalAcceptphase 7 (PromptContributor returns []provider.Block, assembly inside ModelCall, order asserted by index), phase 10 (breakpoint by block index, summary-message breakpoint), phase 11 (memory is RunHook + PromptContributor, WrapModelCall claim removed)
F7Accumulate(seq) cannot serve streaming and discards the partial message the cancel path needsHighAcceptphase 3 (stateful Accumulator with Push/Message/Partial/Usage/Stop), phase 5 and 6 (emit per event while accumulating; persist the partial on failure), phase 12 (parity test uses the same accumulator)
F8Run durability gaps: unpaginated SSE replay, replay-phase drops, seq allocated outside the insert, unhandled Emit errors, queued runs with no liveness pathHighAcceptphase 6 (replay catch-up loop, byte-capped replay stage, seq inside the insert tx, documented Emit error policy with stream_degraded, DB poller, hango.run.oldest_queued_age, runs.last_event_seq in the DDL, turn.aborted before retries, HANGO_SSE_WRITE_TIMEOUT and HANGO_MAX_QUEUED_RUNS_PER_SESSION in config), phase 14 (alerts)
F9Compaction can split a tool_use/tool_result pair; the budget omits system prompt and tool definitionsHighAcceptphase 10 (boundary snap-back, post-compaction validator, adversarial fixture, EstimateRequest, optional TokenCounter over Messages.CountTokens, second overflow degrades to truncation, summary-message cache breakpoint, counters written at the turn boundary, sessions.summary dropped), phase 8 (EstimateRequest introduced with the estimator)
F10State.Private map[string]any has no persistence codec: typed middleware state silently resets on resumeHighAcceptphase 7 (map[string]json.RawMessage, per-middleware accessors, explicit missing-key path with a span event, round-trip test, per-batch scratch cleared at batch end)
F11Broadcaster topic lifetime contradicts HITL suspension; the goroutineleak profile cannot see these leaksHighAcceptphase 6 (reference-counted topics, documented subscriber bound, hango.broadcaster.topics, goroutine-count and topic-count assertions, cancel-registry removal points), phase 16 (goroutineleak only in the soak test)
F12Agent-scoped filesystem exposes every session’s transcript tenant-wide and makes /memories/ a persistent prompt-injection storeHighAcceptphase 8 (per-session prefixes excluded from glob/grep/API, agent-id context key owned by internal/fs, files-API agent ownership check, version column with conflict results, privileged reserved-prefix writes, false parallel-mutation mitigation deleted), phase 9 (risk row corrected), phase 11 (allowed_tools may only narrow), phase 15 (default interrupt rule)
F13Observability hygiene: unimplemented per-tenant trace opt-out, pattern-based content stripping, route-less HTTP metrics, no auth rate limit, unrevocable bootstrap key, missing GenAI constants, collapsed cache-write pricingHighAcceptphase 1 (5 missing GenAI constants, metrics_names.go, content_attrs.go, http.route labeler, collector strip list on both legs), phase 2 (tenants.trace_llm_content, pre-auth limiter, coalesced last_used_at, one-shot bootstrap key), phase 3 (both content gates tested), phase 13 (5m/1h cache-write pricing), phase 14 (retrofit and regex test deleted, alerts added), phases 15 and 16 (dashboard/alert updates)
F14Unjustified scope: CompositeBackend, two bespoke load/soak driver programs, undecided test-database and integration-test wiringHighAcceptphase 11 (CompositeBackend dropped, reserved prefixes in path.go, learning note kept), phase 16 (TestConcurrentRuns/TestSoak replace scripts/, p95 budget written first), phase 2 (per-package test databases via HANGO_TEST_DB_DSN, go test ./... runs integration, -short skips)
F15Internal inconsistencies: wrong dependencies, disagreeing effort tables, duplicate step numbers, unread contracts, wrong package cap, stdlib uuid usageMediumAcceptplan.md (effort table reconciled with learning-doc line items, package cap stated as 18 + tool/builtin, UUID v7 locked), phase 12 ([5, 8, 10]), phase 13 ([9, 10, 12]), phases 3 and 5 (step numbering), phase 5 (Result.Meta deleted), phase 8 (FileStore rename), phase 9 (InterruptConfig defined), phase 10/12 (PromptCaching consumed, ParallelTools deleted), migrations renumbered 00009-00011

Severity breakdown across the 41 underlying findings: 11 Critical, 22 High, 8 Medium. Accepted: 41 (100%). Rejected: 0. Four items were resolved by explicit user decision (D1-D4) rather than by the reviewers’ proposed remedy.

Files modified: plan.md and all sixteen phase files. Net effort change: 180h → 216h (+36h), of which +35h is the learning-doc line item that was previously unbudgeted; implementation effort moved 180h → 181h as scope cuts (BYO credentials, CompositeBackend, load/soak drivers, phase-14 metrics retrofit: −11h) roughly offset new work (run ownership and leases, RLS refactor, SSE durability, authorization matrix, filesystem hardening: +12h).

Whole-Plan Consistency Sweep

Re-read plan.md and all sixteen phase files after the edits, then grepped every plan file for the terms the changes could have stranded.

CheckResult
provider_credentials, HANGO_ENCRYPTION_KEY*, internal/auth/crypt.goOnly in the deliberate D1 deferral notes (phase 12), the RLS table-list comment (phase 16) and the plan.md follow-up. No live requirement, file, step or criterion.
MaxResumes, pause_turn resumeOnly in the phase-5 struct comment recording the removal. All pause_turn mentions now resolve to ErrUnsupportedStop.
CompositeBackendOnly in deepagents comparison prose and the phase-11 “rejected option” learning note. No hango file, step or criterion.
scripts/loadtest, scripts/soaktestOnly in the phase-16 sentence stating they are not built. Makefile targets now wrap go test.
goroutineleak gatesOnly in the phase-16 soak test and the two notes explaining why it is not used elsewhere. Phase 6 uses count-based assertions.
Accumulate(, Accumulate as a functionGone. Phase 3 defines Accumulator; phases 5, 6 and 12 reference it. Remaining word “Accumulate” appears twice as ordinary prose (“accumulate usage”, “accumulate strictly by Index”).
Result.MetaDeleted from the type; only the comment recording the deletion remains. Phase 8 records the offload path on the span instead.
sessions.summaryColumn removed from the phase-10 migration with a comment naming its replacement. No reader anywhere.
FileStoreFactoryRenamed to store.FileStore in phase 8 (two sites) with the rename noted.
hango.tenant.trace_llm (old attribute)Replaced everywhere by hango.tenant.trace_llm_content, declared in phase 1, populated in phase 2, enforced and tested in phase 3, consumed by the collector config in phase 1.
State.Private map[string]anyNow map[string]json.RawMessage in phase 7; the only remaining map[string]any are tool.Parameters() (a JSON Schema object, correct) and goclaw comparison prose.
SystemPromptFragment / Chain.PromptFragmentsReplaced by SystemBlocks in phases 7, 10 and 11, including the phase-7 “Modify” list.
EstimateMessagesReplaced by EstimateRequest in phases 8 and 10, including the phase-10 “Modify” list.
LaneGateReplaced by agent.Gate plus the internal/run semaphore in phase 9.
MarkRunningRemoved: the conditional claim is the transition to running (phase 5).
Phase dependenciesFrontmatter and the plan.md table agree for all sixteen phases, including the corrected 12 → [5, 8, 10] and 13 → [9, 10, 12]. The DAG is acyclic and every phase’s dependencies precede it numerically.
EffortFrontmatter totals match the plan.md table row by row (16, 16, 15, 12, 19, 16, 13, 15, 12, 13, 9, 10, 11, 10, 13, 16) and sum to 216h, matching the plan.md frontmatter. Every phase’s learning-doc outline ends with its time budget, and impl + doc equals the phase total.
Migration numberingRenumbered after the phase-12 migration was dropped: 00001-00008 unchanged, 00009 usage and quotas (13), 00010 pending approvals (15), 00011 RLS (16). No gap and no reordering relative to phase order.
Duplicate step numbersFixed in phase 3 (two “12"s) and phase 5 (two “13"s); every phase’s implementation steps are now strictly increasing.
Package capplan.md now states 18 packages plus internal/tool/builtin, enumerated; the enumeration matches every package named across the phase files, and scripts/ no longer appears as a code location.
Cross-cutting test tablesThe isolation table (phase 2) is extended by phases 4, 5, 6, 8, 9, 13, 15 and now carries both a cross-tenant and an intra-tenant case per table; the authorization matrix (phase 2) is extended by phases 4, 5, 8 and 15.
Locked decisions vs phasesEvery locked decision in plan.md is realized in at least one phase, and no phase contradicts one: one-way package dependency (5, 9), ownership lease (5, 6, 16), pause_turn as error (3, 5), intra-tenant visibility (2, 4, 5, 8, 15), chain-time prompt assembly (7, 10, 11), RLS as a refactored backstop (16), UUID v7 (2).

Unresolved contradictions: 0.

Validation Log

Session 1 — 2026-09-22

Trigger: post-red-team validation interview during /ak:plan --hard. Four questions asked, four answered. Three confirmed the plan as written; one (Q3) replaced a proposed minimal scope with a broader requirement and is propagated through the phase files below.

Q1 — Architecture: Go module path and CI provider

Question: “Go module path? Phase 1 assumes github.com/phihdn/hango and phase 16 assumes GitHub Actions for CI.”

Options: github.com/phihdn/hango (Recommended) | Different path.

Answer: github.com/phihdn/hango.

Rationale: fixes every import path in the plan and the CI provider in phase 16. No plan change.

Q2 — Trade-offs: where LLM-level traces go during development

Question: “Where do LLM-level traces go during development?”

Options: Langfuse Cloud Hobby (Recommended) | Self-host Langfuse from phase 1 | Grafana only for now.

Answer: Langfuse Cloud Hobby.

Rationale: confirms the phase-1 default. Self-hosting stays behind the compose profile, so the decision is reversible without touching application code.

Q3 — Scope: audit trail of admin mutations

Question: “The security review asked for an audit trail of admin mutations (tenant, user, key, quota, approval changes). It is not in the contract.”

Options: Add a minimal audit_events table in phase 2 (Recommended) | Defer to follow-ups.

Answer (custom input, verbatim): “I would like to audit/trace everything. As I said, I want to know what happened when something went wrong”.

Rationale: broader than the recommended option. Every mutation that passes through the API and every approval decision is audited, and each row is correlated to its trace id so the audit trail, the trace, the run event log and the logs all join on one key.

Q4 — Assumptions: phase 3’s working increment

Question: “Phase 3 (provider adapter) has no HTTP surface. Its binary proof is a hango probe command that needs a live Anthropic key, plus fake-provider tests in go test. Acceptable as the phase’s working increment?”

Options: Yes, probe command plus tests (Recommended) | No, add a minimal chat endpoint.

Answer: Yes.

Rationale: a minimal chat endpoint added in phase 3 would be thrown away in phase 5. No plan change.

Confirmed Decisions

#DecisionEffect on the plan
Q1Module path github.com/phihdn/hango, CI on GitHub ActionsNone — confirms phases 1 and 16 as written
Q2Langfuse Cloud Hobby for LLM traces in developmentNone — confirms the phase-1 default; self-host remains a compose profile
Q3Audit everything: every mutating request and every approval decisionNew locked decision; audit_events and the audit middleware land in phase 2; phases 4, 5, 6, 8, 13, 15 declare their actions; phase 14 adds panels, a runbook step and alerts; phase 16 adds the RLS policy and the retention follow-up
Q4Phase 3 ships hango probe plus fake-provider tests, no HTTP surfaceNone — confirms phase 3 as written

Action Items

#ActionWhereStatus
A1Add the audit_events DDL to the existing 00001_identity migration, leaving every other migration number untouchedphase 2Applied
A2Add the audit middleware, SetAuditAction, the AuditStore and the read-only GET /v1/audit-events route with its authorization rowphase 2Applied
A3Register hango.audit.events and hango.audit.write_failures through the phase-1 metrics_names.go mechanism, and add the audit-vs-trace-vs-log learning bulletphase 2Applied
A4Add audit_events to the cross-tenant isolation table and the read endpoint to the authorization matrixphase 2Applied
A5Declare each phase’s audit actions in its implementation steps and assert them in its success criteriaphases 4, 5, 6, 8, 13, 15Applied
A6Audit the sweeper’s approval expiries as actor_kind=system, action approval.expirephase 15Applied
A7Add the Audit panel group to hango-api, the audit step to the runbook, and the denied-rate and write-failure alert rulesphase 14Applied
A8Include audit_events in the RLS enumeration with insert-and-select-only rights, and record the partition-or-archive retention follow-upphase 16Applied
A9Reconcile effort: phase 2 +2h, phases 5, 14 and 15 +1h each; update the phases table and both frontmatter totalsplan.md, phases 2, 5, 14, 15Applied

Impact on Phases

PhaseChangeEffort
2audit_events DDL in 00001_identity, AuditStore, audit middleware and SetAuditAction, GET /v1/audit-events, two metrics, authorization row, isolation case, three risk rows, four success criteria16h → 18h
4Handlers declare agent.create, agent.update, agent.delete, session.create, session.delete; message append deliberately declares none12h (unchanged)
5Run creation declares run.create and records agent, session and lane in detail19h → 20h
6Cancel declares run.cancel; the cancel criterion asserts the audit row16h (unchanged)
8Files API declares file.write and file.delete, path in detail, never content15h (unchanged)
13Quota handler declares quota.update with previous and new limits in detail11h (unchanged)
14Audit panel group on hango-api, runbook step 8, two new alert rules (ten → twelve)10h → 11h
15approval.decide from the handler including denials, approval.expire from the sweeper, learning bullet, success criterion13h → 14h
16audit_events in the RLS enumeration with insert-and-select-only rights, retention follow-up in the deployment guide, OWASP evidence cites audit actions16h (unchanged)

Net effort: 216h → 221h (+5h). Implementation 181h → 186h; learning docs unchanged at 35h.

Whole-Plan Consistency Sweep (Session 1)

Re-grepped every plan file for audit, the action names, the endpoint, the metric names and the migration numbers after the edits.

CheckResult
Table nameaudit_events is the only spelling anywhere; it is created once, in migrations/00001_identity.sql (phase 2), and referenced by phases 4, 5, 6, 8, 13, 14, 15 and 16 and by the plan.md locked decision.
Action namesThe closed set declared in phase 2 is tenant.create, user.create, api_key.create, api_key.revoke, agent.create, agent.update, agent.delete, session.create, session.delete, run.create, run.cancel, file.write, file.delete, quota.update, approval.decide, approval.expire. Every action named in phases 4, 5, 6, 8, 13 and 15 is in that set, and no phase introduces one that is not. The similar-looking approval.requested and approval.decided are SSE event kinds from phase 15, not audit actions, and remain distinct.
EndpointGET /v1/audit-events appears in phase 2 only — the requirement, the authorization matrix, the handler file and the success criterion. No phase adds a write, update or delete route for audit rows.
Metric nameshango.audit.events and hango.audit.write_failures are declared in the phase-2 Instrumentation line, which registers them through the internal/telemetry/metrics_names.go mechanism phase 1 introduced. Phase 1 lists no metric names explicitly, so nothing needed to change there. Phase 14’s dashboard and alert additions use exactly those two names.
Middleware seamSetAuditAction(c, action, targetKind, targetID) is defined once (phase 2 architecture) and called by name in phases 4, 5, 6, 8, 13 and 15. No phase invents a second way to record an action.
Append-only propertyStated in phase 2 (store exposes Append and List only) and enforced at the database level in phase 16 (insert-and-select-only rights for hango_app, plus the test). No phase deletes or updates an audit row, and retention is an explicit v1 non-goal with a named follow-up.
Migration numberingUnchanged: 00001-00011 exactly as before, with audit_events added to the body of 00001 rather than taking a new number. Phase 16’s RLS migration is still 00011.
EffortFrontmatter totals are 16, 18, 15, 12, 20, 16, 13, 15, 12, 13, 9, 10, 11, 11, 14, 16 and sum to 221h, matching the plan.md table Total and the plan.md frontmatter. The Impl column sums to 186h and the Doc column to 35h. Phases 2, 5, 14 and 15 have their per-phase “Time budget” lines updated to match their new frontmatter.
Phase dependenciesUntouched by this session; the DAG is unchanged and still acyclic.
Locked decisions vs phasesThe new “audit everything” decision is realized in phases 2, 4, 5, 6, 8, 13, 14, 15 and 16, and no phase contradicts it.

Unresolved contradictions: 0.

Review Session — 2026-10-13

Trigger: the learning-flow review in plans/reports/review-261013-1855-roadmap-learning-flow-and-gaps.md. The user chose to keep testcontainers (RV1 option O2), adopt the 1 → 3 → 2 → 4 → 5 build order (RV7) and add an evaluation phase (RV9 option O2). All of RV1–RV9 were applied.

#ChangeWhere
RV1Per-package main_test.go starts or reuses one named testcontainers container and creates its own database; HANGO_TEST_DB_DSN is an optional override for CI; no non-test testing.go, so the production binary does not link testcontainersphase 2
RV2The worker builds its scope from the creator’s current role and status and fails the run with creator_inactive when the creator is disabled or gone; the empty role is invalid and visibility(scope) fails closed on itphases 2, 4, 5
RV3Tenant creation, bootstrap and platform routes acting on a tenant write inside that tenant’s scope, so the system pool keeps exactly five callersphases 2, 16
RV4Phase 11 depends on [9, 10]phase 11, plan.md
RV5internal/run/reconcile.go and its test moved from Modify to Createphase 5
RV6Ten stale text spots correcteddocs/roadmap.md, phases 6, 11, 12, 13, 14
RV7Build order section added; the page’s “Next up” follows itplan.md, docs/roadmap.md, build_plan_html.go, plan-template.html
RV8Review checkpoints 5a and 5b inside phase 5phase 5
RV9Phase 17, evaluation loop, 8h + 2h, no new packagephase 17, plan.md, docs/roadmap.md

Net effort: 221h → 231h (+10h, all phase 17). Implementation 186h → 194h, learning docs 35h → 37h.

Decision Session — 2026-10-14

Trigger: the first deployment is for the owner’s family, and the comparison in plans/reports/research-261014-1634-aicoworker-goclaw-hango-comparison.md. The user chose a Telegram channel for the family and a Claude Code CLI runtime limited to the owner. Web chat comes later. Requests disguised as Claude Code with a subscription OAuth token stay out of scope.

#ChangeWhere
DS1Phase 18, Telegram channel: long polling, private chats, a static Telegram-to-hango user map, streamed replies by message edits, run creation shared with the HTTP handlerphase 18, plan.md, docs/roadmap.md
DS2Phase 19, Claude Code CLI runtime: claude -p as a child process for agents with provider = "claude_cli", owner-only checks at agent save, run creation and claim, tools over a loopback MCP bridge, usage excluded from tenant quotasphase 19, plan.md, docs/roadmap.md
DS3Package cap 18 → 20 plus internal/tool/builtin (internal/telegram, internal/claudecli)plan.md
DS4Build order places 18 and 19 after 13plan.md, docs/roadmap.md
DS5The phase-19 MCP bridge is hand-written with the standard library; the official Go SDK is not addedphase 19
DS6Phase 14 depends on 18 and 19 and adds a hango-channels dashboard and three alerts for Telegram and the Claude CLIphase 14, plan.md
DS7Phase 18 adds Markdown-to-HTML replies with a plain-text fallback, edit-failure fallback, retry_after on every call, 409 back-off with a poll watchdog, command registration, repeated typing and reply-to context (+5h)phase 18
DS8Phase 20 brings the Telegram channel near goclaw and OpenClaw: photos, albums, documents, voice notes with transcription, files out, debounce, pairing codes, group chats with mention gating, approval buttons and status reactions; it adds the provider image block and migration 00012, and runs after phase 16phase 20, plan.md, docs/roadmap.md
DS9An LLM gateway with per-user quotas is a follow-up; the family shares one tenant quota until thenplan.md
DS10Phase 20 groups follow goclaw: group pairing, one group principal and one shared session per group or forum topic, open/allowlist/disabled policy, require_mention with strict/yield modes, wake words, pending group history, writers, and an environment → group → topic config merge; forum topics move from follow-ups into phase 20 (+5h)phase 20, plan.md, docs/roadmap.md
DS11Voice transcription uses any OpenAI-compatible endpoint (OpenAI or Groq Whisper) with a HANGO_STT_LANGUAGE hint, vi for the family, NFC-normalized transcripts and a gated Vietnamese fixture test (+1h)phase 20
DS12Group chats are deferred: each person uses a private chat for now, and the goclaw group design moves to a deferred section of phase 20 with HANGO_TELEGRAM_GROUP_HISTORY_LIMIT (default 50, 0 off) overridable per group and topic; the group-principal question stays open until groups return (−9h)phase 20, phase 18, plan.md, docs/roadmap.md
DS13The HTTP layer uses the standard-library http.ServeMux with a small router in internal/server (Handle, Group, an error adapter, DecodeJSON, a status-recording writer) instead of Echo v5; SetAuditAction takes *http.Request, and span names and http.route come from r.Pattern (+1h)phase 1, phase 2, plan.md, docs/tech-stack.md, docs/roadmap.md
DS14The web chat and an admin web page are follow-ups built with Svelte 5 on top of the existing HTTP API; neither is a v1 phase, so effort is unchangedplan.md, docs/roadmap.md, docs/tech-stack.md

Net effort: 231h → 290h (+59h). Implementation 194h → 246h, learning docs 37h → 44h.