Chuyển đến nội dung
hango: agent harness multi-tenant viết bằng Go

hango: agent harness multi-tenant viết bằng Go

Tổng quan

hango là một server agent harness multi-tenant viết từ đầu bằng Go: xác thực bằng API key, run bền vững (durable), streaming qua SSE, và các tính năng harness ghép từ middleware (lập kế hoạch, filesystem ảo, subagent, tóm tắt, memory, skill, human-in-the-loop). Dự án được xây từng tính năng một như một lộ trình học, và trạng thái cuối là sẵn sàng cho production. Cuối mỗi giai đoạn luôn có một binary build, vet và test đều xanh, có ít nhất một integration test chạy trên Postgres thật nếu giai đoạn đó đụng đến store, cùng một tài liệu học tại docs/learning/NN-<slug>.md so sánh lựa chọn của hango với goclaw và langchain deepagents, kèm bản tiếng Việt tại docs/learning/NN-<slug>.vi.md.

Các quyết định đã chốt

  • Go 1.27, net/http của thư viện chuẩn (http.ServeMux cộng một router nhỏ trong internal/server, không dùng web framework), chỉ dùng SSE (không WebSocket). Giới hạn package là 20 cộng internal/tool/builtin, liệt kê chính xác: cmd/hango, migrations, và 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/ không tính vì không chứa code import được. Các package helper chỉ dùng cho test, như internal/store/pg/pgtest, không tính vào giới hạn này. Muốn thêm package thứ hai mươi mốt thì phải sửa dòng này trước khi viết code.
  • Id là UUID v7 từ package uuid của thư viện chuẩn Go 1.27 (uuid.NewV7); uuid.Nil là một hàm và luôn được gọi, không bao giờ so sánh như một giá trị.
  • Hướng phụ thuộc là một chiều: internal/agent không import gì từ internal/run. agent định nghĩa EventSink và Gate; run cài đặt chúng và composition root nối lại với nhau. Thiết kế nào cần chiều ngược lại là thiết kế sai.
  • Việc thực thi run có chủ sở hữu, có lease và có thể tiếp tục. Worker chỉ bắt đầu một run qua một conditional claim (UPDATE ... WHERE state='queued' RETURNING) đặt owner_id và lease_expires_at; mọi chuyển trạng thái đều có điều kiện theo state và owner; reconciliation chỉ thu hồi các lease đã hết hạn và đưa run về hàng đợi tại ranh giới lượt (turn). Khi restart và drain thì requeue, không đánh dấu thất bại.
  • pause_turn không thể xảy ra, nên nó là một lỗi. hango không đăng ký server tool nào của Anthropic, vì vậy pause_turn trả về ErrUnsupportedStop. Không có bộ đếm resume ở bất kỳ đâu.
  • anthropic-sdk-go v1.74 nằm sau provider.Provider do hango tự định nghĩa; mô hình Message/Block/Event đã chuẩn hóa được thiết kế để adapter OpenAI có thể tự tổng hợp block start/stop.
  • Chỉ dùng PostgreSQL qua pgx v5, migration nhúng bằng goose. Từ phase 2.1, query được sqlc sinh ra từ các file SQL; pgx viết tay chỉ được phép cho một query sqlc không biểu đạt được, kèm một comment nói rõ vì sao. Không SQLite, không ORM. Message là các dòng append-only (session_id, seq, role, jsonb blocks).
  • Tenancy logic: tenant_id uuid NOT NULL trên mọi bảng thuộc về tenant, index bắt đầu bằng tenant, scoped store dựng từ context và fail closed (lỗi thì từ chối), có test đọc chéo tenant cho từng bảng ngay từ bảng đầu tiên. RLS là lớp bảo vệ đến muộn, không bao giờ thay thế được các lớp kia, và việc bật nó ở phase 16 là một đợt refactor tường minh sang WithTenantTx cùng một system pool nhỏ có BYPASSRLS cho năm đường đi xảy ra trước khi có tenant scope.
  • Khả năng nhìn thấy dữ liệu trong cùng tenant: một member chỉ thấy và chỉ sửa được những gì mình tạo; owner và admin thấy và sửa được mọi thứ trong tenant của họ. Được thực thi bằng ma trận phân quyền theo từng route cùng một predicate visibility(scope) dùng chung ở store, và được test từng route, từng bảng.
  • hango tự sở hữu danh tính: tenant, user thuộc tenant (owner/admin/member), api_keys thuộc về một user (hash SHA-256 và prefix để tra cứu). Không có mật khẩu, không có giao diện đăng nhập, bootstrap admin key lấy từ config.
  • Run là các dòng bền vững, được thực thi bởi một scheduler lane worker chạy trong tiến trình; SSE subscribe vào event log đã lưu và hỗ trợ tiếp tục bằng Last-Event-ID; có endpoint cancel; có graceful drain. Phiên bản v1 chạy một replica (rate limiter và run registry nằm trong tiến trình, có ghi lại trong tài liệu).
  • Kiến trúc middleware: một slice có thứ tự, điều khiển bằng dữ liệu, với BeforeRun, WrapModelCall, WrapToolCall, ToolProvider, PromptContributor. Mọi tính năng harness sau phase 7 đều là một middleware. System prompt được ghép bên trong chain tại thời điểm gọi model thành []provider.Block từ các contributor theo thứ tự trong stack, nhờ vậy cache breakpoint và thứ tự memory có thể diễn đạt bằng chỉ số block. State của middleware là map[string]json.RawMessage do chính middleware đó marshal, nên nó sống sót qua persistence và resume. Tool trả về Result{ForLLM, ForUser, IsError}; lỗi của tool trở thành tool result, không bao giờ panic vào loop; có phát hiện gọi lặp. Không có system prompt gốc do hango tự viết: chính sách nằm trong mô tả tool, system prompt giữ ngắn và động.
  • Audit mọi thứ: mọi API request có thay đổi dữ liệu và mọi quyết định approval đều ghi một dòng `audit_events` append-only mang theo request id, trace id và span id; các dòng audit, run event, span và log nối với nhau qua `trace_id`.
  • Observability stack: OTel trong tiến trình (một TracerProvider/MeterProvider/LoggerProvider, otelslog làm handler slog mặc định) → một đường xuất OTLP duy nhất tới OpenTelemetry Collector → fan-out sang Grafana LGTM (Tempo/Loki/Prometheus) và Langfuse Cloud; tên attribute GenAI và langfuse.* là các hằng số cục bộ của hango, nội dung prompt mặc định tắt và bị collector loại bỏ. Observability là yêu cầu nền tảng: nó ra mắt ở phase 1 và mọi phase đều tự gắn instrumentation, để bất kỳ lỗi nào cũng định vị được theo chuỗi tenant → agent → session → run → turn → model call → tool call.

Mục tiêu

#Mục tiêuƯu tiên
1Server harness multi-tenant sẵn sàng cho production, một binary duy nhất, chạy bằng docker compose up; truy cập chéo tenant là bất khả thi do cách thiết kế, được chứng minh theo từng bảngP1
2Run bền vững, hủy được, tiếp tục được, có streaming SSE; tầng provider abstraction được chứng minh bằng adapter thứ hai (tương thích OpenAI)P1
3Các tính năng harness ghép từ middleware: lập kế hoạch, filesystem, subagent, quản lý context, memory, skill, HITLP1
4Mọi tầng đều quan sát được và mọi lỗi đều định vị được trong Grafana và Langfuse; có thống kê usage và quotaP1
5Một tài liệu học cho mỗi giai đoạn: khái niệm, hango so với goclaw và deepagents, cách debug tầng đóP1

Các giai đoạn

Effort được ghi dưới dạng phần cài đặt cộng tài liệu học, vì tài liệu học là một sản phẩm bàn giao của mọi giai đoạn, không phải việc làm thêm cho có.

#Giai đoạnImplDocEffortPhụ thuộc
1Nền tảng, khung server, observability spine15h2h17h—
2Persistence và lõi tenancy16h2h18h1
2.1Chuyển store sang sqlc6h1h7h2
3Provider abstraction và adapter Anthropic12h3h15h1
4Agent, session, message10h2h12h2, 2.1
5Tool, agent loop, run bền vững17h3h20h3, 4
6Streaming, hủy run, drain14h2h16h5
7Middleware chain và tool lập kế hoạch11h2h13h6
8Filesystem backend và offload kết quả lớn13h2h15h7
9Subagent10h2h12h8
10Quản lý context và tóm tắt11h2h13h8
11Memory dài hạn và skill7h2h9h9, 10
12Provider tương thích OpenAI và provider registry8h2h10h5, 8, 10
13Usage, quota, rate limit9h2h11h9, 10, 12
14Dashboard, alert, runbook debug10h2h12h13, 18, 19
15Human-in-the-loop12h2h14h9, 14
16Làm cứng cho production và lớp bảo vệ RLS13h3h16h15
17Vòng đánh giá (evaluation loop)8h2h10h16
18Kênh Telegram15h2h17h6, 13
19Runtime Claude Code CLI cho owner12h2h14h6, 8, 13
20Telegram: media, pairing và approval23h3h26h15, 16, 18
Tổng252h45h297h

Thứ tự xây dựng

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

Phase 3 chỉ phụ thuộc vào phase 1, nên nó chạy trước phase 2. Nhờ vậy một model thật stream vào Tempo và Langfuse ở mốc khoảng 31h thay vì 49h, và observability spine có thứ đáng xem từ sớm. Số phase, phạm vi và phụ thuộc không đổi. Phase 18 và 19 chạy ngay sau phase 13, để gia đình có thể dùng hango qua Telegram trước khi làm dashboard, approval và hardening; cả hai đều không thêm migration, nên các migration 00001–00011 giữ nguyên số. Phase 20 chạy sau phase 16, nên migration 00012_telegram.sql của nó đứng sau migration RLS và tạo các bảng với row-level security ngay từ đầu, và các eval của phase 17 có thể có một task về hình ảnh. build_plan_html.go đọc dòng này, và mục “Next up” của trang đi theo nó.

Phase 2.1 được thêm sau khi phase 2 đã merge (issue #26). Nó chuyển store của phase 2 sang sqlc trước khi phase 4 sao chép pattern của nó, và không thêm migration nào, nên không số migration nào đổi.

Phase 5 dừng lại hai lần để review: 5a (lõi harness: tool và loop chạy với provider.Fake) và 5b (độ bền: run, claim, lease, reclaim, worker, endpoint). File của phase ghi rõ bước và tiêu chí nào thuộc về checkpoint nào.

Tiêu chí nghiệm thu

  • Server khởi động từ config, tắt êm (graceful shutdown), có health endpoint.
  • Tenant được xác định từ API key; mọi lời gọi store và tool đều bị giới hạn trong tenant. Truy cập chéo tenant là bất khả thi do cách thiết kế, được test bao phủ.
  • Người dùng trong một tenant có thể tạo và chạy agent.
  • Agent loop có gọi tool và streaming qua HTTP.
  • Session và message được lưu bền vững.
  • Subagent, tool todo/lập kế hoạch, filesystem ảo theo từng tenant, memory dài hạn.
  • Trace và log có cấu trúc của OpenTelemetry, đối chiếu được với nhau trong Grafana và Langfuse.
  • go test ./... xanh, go vet sạch.
  • docker compose up dựng lên server và database.
  • hango eval run chấm điểm một bộ task cố định qua các run thật, đẩy điểm lên Langfuse, và hango eval compare cho thấy một thay đổi có cải thiện được kết quả hay không.

Tài liệu học

Mỗi giai đoạn viết docs/learning/NN-<slug>.md (khái niệm, lựa chọn thiết kế, đánh đổi, cách goclaw và deepagents làm cùng việc đó, vì sao hango đi khác, cách debug tầng đó) và cập nhật docs/learning/README.md cùng docs/system-architecture.md. Mọi tài liệu dưới docs/ đều bắt đầu bằng front matter có title và weight (weight của một tài liệu học là số phase của nó nhân 10) để site Hugo trong site/ sắp xếp được; xem docs/docs-site.md.

Site có hai ngôn ngữ. Mỗi tài liệu học đi kèm một bản tiếng Việt docs/learning/NN-<slug>.vi.md trong cùng pull request, và mỗi bài nhật ký trong plans/journals/ cũng vậy. ak journal create chỉ viết tiếng Anh, nên file .vi.md được viết tay sau đó.

Các việc tiếp theo ngoài phạm vi

  • Tự mang provider credential theo từng tenant (BYO) (hoãn từ phase 12). Các yêu cầu mà công việc này phải đáp ứng: một bảng provider_credentials với ciphertext AES-GCM mà AAD gắn với (tenant_id, provider) để một dòng không thể bị phát lại sang tenant khác; một key id cho mỗi dòng với khóa primary/secondary để rotate khóa; resolve qua seam CredentialLookup hiện có và không cache trong bộ nhớ các secret đã giải mã; allowlist chống SSRF cộng việc từ chối IP riêng sau khi resolve đối với base_url do tenant cung cấp; response chỉ chứa metadata; chỉ owner/admin được dùng. Credential của server do operator đặt vẫn là đường mặc định.
  • Server tool của Anthropic và việc truyền nguyên block thô (opaque raw-block passthrough), hai thứ này cùng nhau mới làm cho pause_turn có thể xảy ra và tiếp tục được. Cho đến lúc đó pause_turn là ErrUnsupportedStop.
  • LLM gateway với quota theo từng user. Một nơi duy nhất nắm provider credential, định tuyến mỗi lời gọi tới một provider, và áp quota theo từng user chứ không chỉ theo tenant, để các thành viên trong gia đình không dùng hết ngân sách của nhau và có thể thêm provider mà không đụng vào agent loop. Nó xây trên registry của phase 12 và các bản ghi usage của phase 13. Cho đến lúc đó cả gia đình dùng chung một quota của tenant.
  • MCP bridge (MCP server được phơi ra thành các cài đặt của tool.Tool); deploy nhiều replica (rate limiter dùng chung, fan-out event; lưu ý lease sở hữu run đã an toàn cho nhiều replica); tool thực thi trong sandbox (backend Sandbox có Execute); thực thi tool song song trong một batch; các kênh chat khác ngoài Telegram, chat nhóm Telegram (thiết kế goclaw được giữ trong phase 20, với giới hạn lịch sử cấu hình được), Telegram webhook mode và multi-bot, giao diện web chat (dự kiến làm sau Telegram) và một trang web admin, cả hai viết bằng Svelte 5 và nói chuyện với cùng HTTP API, ứng dụng desktop, i18n, hơn 20 provider.
  • Context inspector (spec), làm sau trang web admin. Hiển thị request model kế tiếp của một session thành các phần có gắn nhãn kèm số token, lưu các override theo agent và theo session để loại bỏ hoặc thay thế từng phần, và chạy các lệnh gọi thử một lần. Giao diện chính là một bảng checkbox trên trang web admin, kèm CLI hango context dùng cùng API.

Red Team Review

Bốn đợt review đối kháng (security adversary, failure-mode analyst, assumption destroyer, scope and complexity critic) đưa ra 41 phát hiện đối với 16 file phase, được gom lại bên dưới thành 15 nhóm chủ đề cùng bốn quyết định do người dùng trực tiếp đưa ra. Mọi nhóm đều được chấp nhận. Báo cáo: plans/reports/redteam-260922-0126-{security-adversary,failure-mode-analyst,assumption-destroyer,scope-complexity-critic}.md.

#Phát hiệnMức độXử lýÁp dụng cho
D1BYO provider credential theo từng tenant (bảng, AES-GCM, rotate khóa, 3 endpoint) là phạm vi vượt ngoài hợp đồng và là bề mặt bảo mật không giới hạnCaoChấp nhận (quyết định của người dùng)phase 12 (cắt phạm vi còn seam CredentialLookup trên config của operator, base URL được kiểm tra là https hoặc loopback), phase 16 (danh sách bảng RLS, hướng dẫn deploy), mục follow-up của plan.md
D2Không có mô hình phân quyền trong cùng tenant: một member có thể đọc và phá hủy dữ liệu của mọi user khácNghiêm trọngChấp nhận (quyết định của người dùng)phase 2 (ma trận route, các case test trong cùng tenant), phase 4 (sessions.created_by, ma trận agent/session/message, trả 404 chứ không phải 403), phase 5 và 6 (run và event), phase 8 (files API), phase 15 (approval)
D3Restart hoặc drain khi đang running làm mất việc hoặc thực thi run hai lần; không có lease sở hữuNghiêm trọngChấp nhận (quyết định của người dùng)phase 5 (owner_id, lease_expires_at, conditional claim, heartbeat, resume_turn, reclaim sweep, tool idempotency), phase 6 (drain requeue, HANGO_DRAIN_TIMEOUT ≥ một turn), phase 16 (hướng dẫn deploy)
D4pause_turn không thể round-trip qua mô hình block của hango, và hango không đăng ký server tool nào, nên đường resume là code không thể chạm tớiNghiêm trọngChấp nhận (quyết định của người dùng)phase 3 (ErrUnsupportedStop), phase 5 (xóa MaxResumes, cập nhật fixture và tiêu chí), các quyết định đã chốt và mục follow-up của plan.md
F1RLS như đã thiết kế từ chối mọi query trên pool thường, bắt đầu từ việc xác thực API keyNghiêm trọngChấp nhậnphase 16 (system role có BYPASSRLS trên pool thứ hai nhỏ với năm nơi gọi được liệt kê, refactor WithTenantTx cho mọi call site thuộc tenant, không bao giờ băng qua một lời gọi LLM, toàn bộ test suite xanh khi ép RLS, đường auth được chạy thử sau khi bật RLS)
F2Không có atomic run claim và các chuyển trạng thái approval/expiry không nằm trong transaction: run có thể chạy hai lầnNghiêm trọngChấp nhậnphase 5 (conditional claim, chuyển trạng thái có điều kiện, enqueue dedup, test race hai worker), phase 15 (quyết định và expiry mỗi bên chuyển trạng thái trong transaction riêng; hai approval cuối đồng thời ⇒ chỉ một lần thực thi)
F3Khi resume sau approval, RepairToolCalls phá hủy chính batch mà nó định resume; creator tự approve; tham số edit không được kiểm traNghiêm trọngChấp nhậnphase 15 (nạp các quyết định trước BeforeRun, repair bỏ qua các id đã quyết định, resume chạy lại batch đang chờ qua tool chain, tiêu chí exactly-once, chỉ owner/admin và không bao giờ là creator, edit được kiểm tra theo Parameters() và bộ khớp when, approval.decided do handler publish)
F4Semaphore lane của subagent được acquire kiểu re-entrant: tự deadlock ở depth ≥ 2Nghiêm trọngChấp nhậnphase 9 (TryAcquire không chặn khi depth > 0 với tool result báo hết capacity, các batch tool chạy tuần tự trong v1, xóa tiêu chí và test parallel-task, giữ giới hạn depth)
F5internal/agent và internal/run tạo thành vòng import; chữ ký Agent.Run đổi ở phase 9 mà không liệt kê các nơi gọiNghiêm trọngChấp nhậncác quyết định đã chốt của plan.md (quy tắc một chiều), phase 5 (RunOptions, chữ ký cuối cùng, liệt kê các call site, EventSink thuộc về agent), phase 9 (agent.Gate được inject, không đổi chữ ký)
F6Thứ tự “Memory đứng sau PromptCache” mang tính then chốt lại không thể tồn tại: các prompt fragment là string được ghép bên ngoài chainNghiêm trọngChấp nhậnphase 7 (PromptContributor trả về []provider.Block, ghép bên trong ModelCall, thứ tự được assert theo chỉ số), phase 10 (breakpoint theo chỉ số block, breakpoint cho summary message), phase 11 (memory là RunHook + PromptContributor, bỏ tuyên bố WrapModelCall)
F7Accumulate(seq) không phục vụ được streaming và vứt mất message dở dang mà đường cancel cầnCaoChấp nhậnphase 3 (Accumulator có trạng thái với Push/Message/Partial/Usage/Stop), phase 5 và 6 (emit theo từng event trong khi tích lũy; lưu phần dở dang khi lỗi), phase 12 (parity test dùng cùng accumulator)
F8Lỗ hổng về độ bền của run: SSE replay không phân trang, rớt event trong giai đoạn replay, seq được cấp phát ngoài lệnh insert, lỗi Emit không được xử lý, run queued không có đường kiểm tra sốngCaoChấp nhậnphase 6 (vòng lặp catch-up cho replay, replay stage giới hạn theo byte, seq nằm trong tx insert, chính sách lỗi Emit có tài liệu với stream_degraded, DB poller, hango.run.oldest_queued_age, runs.last_event_seq trong DDL, turn.aborted trước khi retry, HANGO_SSE_WRITE_TIMEOUT và HANGO_MAX_QUEUED_RUNS_PER_SESSION trong config), phase 14 (alert)
F9Compaction có thể cắt đôi một cặp tool_use/tool_result; ngân sách không tính system prompt và định nghĩa toolCaoChấp nhậnphase 10 (snap-back về ranh giới, validator sau compaction, fixture đối kháng, EstimateRequest, TokenCounter tùy chọn trên Messages.CountTokens, lần tràn thứ hai thì hạ cấp xuống truncation, cache breakpoint cho summary message, các counter ghi tại ranh giới turn, bỏ sessions.summary), phase 8 (EstimateRequest được giới thiệu cùng estimator)
F10State.Private map[string]any không có codec persistence: state middleware có kiểu bị reset im lặng khi resumeCaoChấp nhậnphase 7 (map[string]json.RawMessage, accessor theo từng middleware, đường missing-key tường minh với span event, test round-trip, scratch theo batch được xóa khi batch kết thúc)
F11Vòng đời topic của broadcaster mâu thuẫn với việc HITL tạm dừng; profile goroutineleak không thấy được các rò rỉ nàyCaoChấp nhậnphase 6 (topic đếm tham chiếu, giới hạn subscriber có tài liệu, hango.broadcaster.topics, assert số goroutine và số topic, các điểm gỡ khỏi cancel registry), phase 16 (goroutineleak chỉ trong soak test)
F12Filesystem theo phạm vi agent phơi bày transcript của mọi session trên toàn tenant và biến /memories/ thành kho prompt-injection bền vữngCaoChấp nhậnphase 8 (prefix theo từng session bị loại khỏi glob/grep/API, context key agent-id do internal/fs sở hữu, kiểm tra quyền sở hữu agent trong files API, cột version với kết quả conflict, ghi vào prefix dành riêng cần đặc quyền, xóa biện pháp giảm thiểu sai về parallel-mutation), phase 9 (sửa dòng rủi ro), phase 11 (allowed_tools chỉ được thu hẹp), phase 15 (quy tắc interrupt mặc định)
F13Vệ sinh observability: tắt trace theo tenant chưa được cài đặt, lọc nội dung dựa trên pattern, metric HTTP không có route, không giới hạn tốc độ auth, bootstrap key không revoke được, thiếu hằng số GenAI, giá cache-write bị gộpCaoChấp nhậnphase 1 (5 hằng số GenAI còn thiếu, metrics_names.go, content_attrs.go, bộ gắn nhãn http.route, danh sách strip của collector ở cả hai nhánh), phase 2 (tenants.trace_llm_content, limiter trước auth, last_used_at được gộp, bootstrap key dùng một lần), phase 3 (test cả hai cổng nội dung), phase 13 (giá cache-write 5m/1h), phase 14 (bỏ retrofit và test regex, thêm alert), phase 15 và 16 (cập nhật dashboard/alert)
F14Phạm vi không có lý do: CompositeBackend, hai chương trình driver load/soak riêng, cách nối test database và integration test chưa quyết địnhCaoChấp nhậnphase 11 (bỏ CompositeBackend, prefix dành riêng nằm trong path.go, giữ ghi chú học), phase 16 (TestConcurrentRuns/TestSoak thay cho scripts/, viết ngân sách p95 trước), phase 2 (test database theo từng package qua HANGO_TEST_DB_DSN, go test ./... chạy integration, -short thì bỏ qua)
F15Mâu thuẫn nội bộ: phụ thuộc sai, các bảng effort không khớp nhau, số bước bị trùng, hợp đồng không ai đọc, giới hạn package sai, cách dùng uuid của thư viện chuẩnTrung bìnhChấp nhậnplan.md (bảng effort được đối soát với các dòng tài liệu học, giới hạn package ghi là 18 + tool/builtin, chốt UUID v7), phase 12 ([5, 8, 10]), phase 13 ([9, 10, 12]), phase 3 và 5 (đánh số bước), phase 5 (xóa Result.Meta), phase 8 (đổi tên FileStore), phase 9 (định nghĩa InterruptConfig), phase 10/12 (dùng PromptCaching, xóa ParallelTools), đánh số lại migration 00009-00011

Phân bố mức độ trên 41 phát hiện gốc: 11 Nghiêm trọng, 22 Cao, 8 Trung bình. Được chấp nhận: 41 (100%). Bị từ chối: 0. Bốn mục được giải quyết bằng quyết định tường minh của người dùng (D1-D4) thay vì cách khắc phục mà các reviewer đề xuất.

File đã sửa: plan.md và cả mười sáu file phase. Effort ròng thay đổi: 180h → 216h (+36h), trong đó +35h là mục tài liệu học trước đó chưa được dự toán; effort cài đặt chuyển từ 180h → 181h vì các lần cắt phạm vi (BYO credential, CompositeBackend, driver load/soak, retrofit metric của phase 14: −11h) gần như bù cho công việc mới (sở hữu run và lease, refactor RLS, độ bền SSE, ma trận phân quyền, làm cứng filesystem: +12h).

Rà soát tính nhất quán toàn kế hoạch

Đọc lại plan.md và cả mười sáu file phase sau các lần sửa, rồi grep mọi file của kế hoạch để tìm các thuật ngữ mà những thay đổi có thể đã để lại mồ côi.

Kiểm traKết quả
provider_credentials, HANGO_ENCRYPTION_KEY*, internal/auth/crypt.goChỉ nằm trong các ghi chú hoãn D1 có chủ đích (phase 12), chú thích danh sách bảng RLS (phase 16) và mục follow-up của plan.md. Không còn yêu cầu, file, bước hay tiêu chí nào đang hiệu lực.
MaxResumes, pause_turn resumeChỉ nằm trong chú thích struct của phase 5 ghi lại việc đã xóa. Mọi chỗ nhắc pause_turn giờ đều trỏ về ErrUnsupportedStop.
CompositeBackendChỉ nằm trong văn xuôi so sánh với deepagents và ghi chú học “phương án bị loại” của phase 11. Không có file, bước hay tiêu chí nào của hango.
scripts/loadtest, scripts/soaktestChỉ nằm trong câu của phase 16 nói rằng chúng không được xây. Các target Makefile giờ bọc go test.
Các cổng goroutineleakChỉ nằm trong soak test của phase 16 và hai ghi chú giải thích vì sao không dùng ở nơi khác. Phase 6 dùng assert dựa trên số đếm.
Accumulate(, Accumulate như một hàmĐã biến mất. Phase 3 định nghĩa Accumulator; phase 5, 6 và 12 tham chiếu tới nó. Từ “Accumulate” còn lại xuất hiện hai lần như văn xuôi bình thường (“accumulate usage”, “accumulate strictly by Index”).
Result.MetaĐã xóa khỏi kiểu; chỉ còn chú thích ghi lại việc xóa. Phase 8 ghi đường offload lên span thay thế.
sessions.summaryCột đã bị bỏ khỏi migration phase 10 kèm chú thích nêu tên thứ thay thế. Không còn nơi nào đọc.
FileStoreFactoryĐổi tên thành store.FileStore ở phase 8 (hai chỗ), có ghi chú việc đổi tên.
hango.tenant.trace_llm (attribute cũ)Được thay ở mọi nơi bằng hango.tenant.trace_llm_content, khai báo ở phase 1, điền ở phase 2, thực thi và test ở phase 3, được cấu hình collector của phase 1 dùng.
State.Private map[string]anyGiờ là map[string]json.RawMessage ở phase 7; các map[string]any còn lại chỉ là tool.Parameters() (một đối tượng JSON Schema, đúng) và văn xuôi so sánh với goclaw.
SystemPromptFragment / Chain.PromptFragmentsĐược thay bằng SystemBlocks ở phase 7, 10 và 11, kể cả danh sách “Modify” của phase 7.
EstimateMessagesĐược thay bằng EstimateRequest ở phase 8 và 10, kể cả danh sách “Modify” của phase 10.
LaneGateĐược thay bằng agent.Gate cộng semaphore của internal/run ở phase 9.
MarkRunningĐã bỏ: conditional claim chính là bước chuyển sang running (phase 5).
Phụ thuộc giữa các phaseFrontmatter và bảng của plan.md khớp nhau cho cả mười sáu phase, kể cả phase 12 → [5, 8, 10] và phase 13 → [9, 10, 12] đã sửa. DAG không có chu trình và phụ thuộc của mỗi phase đều đứng trước nó về số thứ tự.
EffortTổng trong frontmatter khớp với bảng plan.md từng dòng (16, 16, 15, 12, 19, 16, 13, 15, 12, 13, 9, 10, 11, 10, 13, 16) và cộng lại 216h, khớp với frontmatter của plan.md. Dàn ý tài liệu học của mỗi phase kết thúc bằng ngân sách thời gian, và impl + doc bằng tổng của phase.
Đánh số migrationĐược đánh số lại sau khi migration phase 12 bị bỏ: 00001-00008 không đổi, 00009 usage và quota (13), 00010 pending approvals (15), 00011 RLS (16). Không có khoảng trống và không đảo thứ tự so với thứ tự phase.
Số bước bị trùngĐã sửa ở phase 3 (hai bước “12”) và phase 5 (hai bước “13”); các bước cài đặt của mọi phase giờ tăng nghiêm ngặt.
Giới hạn packageplan.md giờ ghi 18 package cộng internal/tool/builtin, có liệt kê; danh sách khớp với mọi package được nêu trong các file phase, và scripts/ không còn xuất hiện như nơi chứa code.
Các bảng test xuyên suốtBảng isolation (phase 2) được các phase 4, 5, 6, 8, 9, 13, 15 mở rộng và giờ có cả case chéo tenant lẫn case trong cùng tenant cho mỗi bảng; ma trận phân quyền (phase 2) được các phase 4, 5, 8 và 15 mở rộng.
Các quyết định đã chốt so với các phaseMỗi quyết định đã chốt trong plan.md đều được hiện thực ở ít nhất một phase, và không phase nào mâu thuẫn với chúng: phụ thuộc package một chiều (5, 9), lease sở hữu (5, 6, 16), pause_turn là lỗi (3, 5), khả năng nhìn thấy trong cùng tenant (2, 4, 5, 8, 15), ghép prompt tại thời điểm chain (7, 10, 11), RLS như lớp bảo vệ được refactor (16), UUID v7 (2).

Mâu thuẫn chưa giải quyết: 0.

Nhật ký xác thực

Phiên 1 — 2026-09-22

Nguyên nhân: cuộc phỏng vấn xác thực sau red-team trong /ak:plan --hard. Hỏi bốn câu, được trả lời bốn câu. Ba câu xác nhận kế hoạch như đã viết; một câu (Q3) thay phạm vi tối thiểu được đề xuất bằng một yêu cầu rộng hơn và được lan truyền qua các file phase bên dưới.

Q1 — Kiến trúc: đường dẫn Go module và nhà cung cấp CI

Câu hỏi: “Go module path? Phase 1 giả định github.com/phihdn/hango và phase 16 giả định GitHub Actions cho CI.”

Lựa chọn: github.com/phihdn/hango (Khuyến nghị) | Đường dẫn khác.

Trả lời: github.com/phihdn/hango.

Lý do: cố định mọi import path trong kế hoạch và nhà cung cấp CI ở phase 16. Kế hoạch không đổi.

Q2 — Đánh đổi: trace mức LLM đi đâu trong lúc phát triển

Câu hỏi: “Trace mức LLM đi đâu trong lúc phát triển?”

Lựa chọn: Langfuse Cloud Hobby (Khuyến nghị) | Tự host Langfuse từ phase 1 | Tạm thời chỉ dùng Grafana.

Trả lời: Langfuse Cloud Hobby.

Lý do: xác nhận mặc định của phase 1. Việc tự host vẫn nằm sau compose profile, nên quyết định này đảo ngược được mà không đụng vào code ứng dụng.

Q3 — Phạm vi: audit trail cho các thay đổi của admin

Câu hỏi: “Bản review bảo mật yêu cầu một audit trail cho các thay đổi của admin (tenant, user, key, quota, approval). Nó không nằm trong hợp đồng.”

Lựa chọn: Thêm một bảng audit_events tối thiểu ở phase 2 (Khuyến nghị) | Hoãn sang follow-up.

Trả lời (nhập tùy ý, nguyên văn): “I would like to audit/trace everything. As I said, I want to know what happened when something went wrong”.

Lý do: rộng hơn phương án được khuyến nghị. Mọi thay đổi đi qua API và mọi quyết định approval đều được audit, và mỗi dòng được gắn với trace id để audit trail, trace, run event log và log nối với nhau qua một khóa duy nhất.

Q4 — Giả định: phần tăng trưởng chạy được của phase 3

Câu hỏi: “Phase 3 (provider adapter) không có bề mặt HTTP. Bằng chứng binary của nó là lệnh hango probe cần một Anthropic key thật, cộng các test fake-provider trong go test. Chấp nhận được làm phần tăng trưởng chạy được của phase không?”

Lựa chọn: Có, lệnh probe cộng test (Khuyến nghị) | Không, thêm một chat endpoint tối thiểu.

Trả lời: Có.

Lý do: một chat endpoint tối thiểu thêm ở phase 3 sẽ bị vứt đi ở phase 5. Kế hoạch không đổi.

Các quyết định đã xác nhận

#Quyết địnhẢnh hưởng đến kế hoạch
Q1Module path github.com/phihdn/hango, CI trên GitHub ActionsKhông có — xác nhận phase 1 và 16 như đã viết
Q2Langfuse Cloud Hobby cho trace LLM trong lúc phát triểnKhông có — xác nhận mặc định của phase 1; tự host vẫn là một compose profile
Q3Audit mọi thứ: mọi request có thay đổi dữ liệu và mọi quyết định approvalQuyết định đã chốt mới; audit_events và audit middleware vào phase 2; các phase 4, 5, 6, 8, 13, 15 khai báo action của mình; phase 14 thêm panel, một bước runbook và alert; phase 16 thêm policy RLS và follow-up về retention
Q4Phase 3 ra mắt hango probe cộng test fake-provider, không có bề mặt HTTPKhông có — xác nhận phase 3 như đã viết

Hạng mục hành động

#Hành độngỞ đâuTrạng thái
A1Thêm DDL audit_events vào migration 00001_identity hiện có, giữ nguyên số của mọi migration khácphase 2Đã áp dụng
A2Thêm audit middleware, SetAuditAction, AuditStore và route chỉ đọc GET /v1/audit-events cùng dòng phân quyền của nóphase 2Đã áp dụng
A3Đăng ký hango.audit.events và hango.audit.write_failures qua cơ chế metrics_names.go của phase 1, và thêm bullet học về audit so với trace so với logphase 2Đã áp dụng
A4Thêm audit_events vào bảng isolation chéo tenant và endpoint đọc vào ma trận phân quyềnphase 2Đã áp dụng
A5Khai báo các audit action của mỗi phase trong các bước cài đặt và assert chúng trong tiêu chí thành côngphase 4, 5, 6, 8, 13, 15Đã áp dụng
A6Audit các lần approval hết hạn do sweeper thực hiện với actor_kind=system, action approval.expirephase 15Đã áp dụng
A7Thêm nhóm panel Audit vào hango-api, bước audit vào runbook, và các alert rule cho tỷ lệ bị từ chối và lỗi ghiphase 14Đã áp dụng
A8Đưa audit_events vào danh sách RLS với quyền chỉ insert và select, và ghi lại follow-up về retention theo kiểu partition hoặc archivephase 16Đã áp dụng
A9Đối soát effort: phase 2 +2h, phase 5, 14 và 15 mỗi phase +1h; cập nhật bảng các phase và cả hai tổng trong frontmatterplan.md, phase 2, 5, 14, 15Đã áp dụng

Tác động lên các phase

PhaseThay đổiEffort
2DDL audit_events trong 00001_identity, AuditStore, audit middleware và SetAuditAction, GET /v1/audit-events, hai metric, dòng phân quyền, case isolation, ba dòng rủi ro, bốn tiêu chí thành công16h → 18h
4Các handler khai báo agent.create, agent.update, agent.delete, session.create, session.delete; việc append message cố ý không khai báo gì12h (không đổi)
5Tạo run khai báo run.create và ghi agent, session và lane vào detail19h → 20h
6Cancel khai báo run.cancel; tiêu chí cancel assert dòng audit16h (không đổi)
8Files API khai báo file.write và file.delete, đường dẫn trong detail, không bao giờ ghi nội dung15h (không đổi)
13Handler quota khai báo quota.update với giới hạn cũ và mới trong detail11h (không đổi)
14Nhóm panel Audit trên hango-api, bước 8 của runbook, hai alert rule mới (mười → mười hai)10h → 11h
15approval.decide từ handler kể cả các lần từ chối, approval.expire từ sweeper, bullet học, tiêu chí thành công13h → 14h
16audit_events trong danh sách RLS với quyền chỉ insert và select, follow-up retention trong hướng dẫn deploy, bằng chứng OWASP trích dẫn các audit action16h (không đổi)

Effort ròng: 216h → 221h (+5h). Cài đặt 181h → 186h; tài liệu học không đổi ở 35h.

Rà soát tính nhất quán toàn kế hoạch (Phiên 1)

Grep lại mọi file của kế hoạch để tìm audit, tên các action, endpoint, tên metric và số migration sau các lần sửa.

Kiểm traKết quả
Tên bảngaudit_events là cách viết duy nhất ở mọi nơi; nó được tạo một lần, trong migrations/00001_identity.sql (phase 2), và được các phase 4, 5, 6, 8, 13, 14, 15 và 16 cùng quyết định đã chốt của plan.md tham chiếu.
Tên actionTập đóng được khai báo ở phase 2 là 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. Mọi action được nêu ở phase 4, 5, 6, 8, 13 và 15 đều nằm trong tập đó, và không phase nào đưa thêm action ngoài tập. approval.requested và approval.decided trông giống nhưng là loại SSE event của phase 15, không phải audit action, và vẫn tách biệt.
EndpointGET /v1/audit-events chỉ xuất hiện ở phase 2 — yêu cầu, ma trận phân quyền, file handler và tiêu chí thành công. Không phase nào thêm route ghi, sửa hay xóa dòng audit.
Tên metrichango.audit.events và hango.audit.write_failures được khai báo trong dòng Instrumentation của phase 2, đăng ký qua cơ chế internal/telemetry/metrics_names.go mà phase 1 giới thiệu. Phase 1 không liệt kê tên metric nào một cách tường minh, nên không cần đổi gì. Các bổ sung dashboard và alert của phase 14 dùng đúng hai tên này.
Seam middlewareSetAuditAction(c, action, targetKind, targetID) được định nghĩa một lần (kiến trúc phase 2) và được gọi theo tên ở phase 4, 5, 6, 8, 13 và 15. Không phase nào tự nghĩ ra cách thứ hai để ghi một action.
Tính append-onlyĐược nêu ở phase 2 (store chỉ lộ Append và List) và được thực thi ở mức database ở phase 16 (quyền chỉ insert và select cho hango_app, cộng test). Không phase nào xóa hay sửa một dòng audit, và retention là một non-goal tường minh của v1 với follow-up có tên.
Đánh số migrationKhông đổi: 00001-00011 y như trước, với audit_events được thêm vào thân của 00001 thay vì lấy một số mới. Migration RLS của phase 16 vẫn là 00011.
EffortTổng trong frontmatter là 16, 18, 15, 12, 20, 16, 13, 15, 12, 13, 9, 10, 11, 11, 14, 16 và cộng lại 221h, khớp với Tổng của bảng plan.md và frontmatter của plan.md. Cột Impl cộng lại 186h và cột Doc 35h. Các dòng “Time budget” của từng phase 2, 5, 14 và 15 đã được cập nhật cho khớp frontmatter mới.
Phụ thuộc giữa các phasePhiên này không đụng tới; DAG không đổi và vẫn không có chu trình.
Các quyết định đã chốt so với các phaseQuyết định mới “audit mọi thứ” được hiện thực ở các phase 2, 4, 5, 6, 8, 13, 14, 15 và 16, và không phase nào mâu thuẫn với nó.

Mâu thuẫn chưa giải quyết: 0.

Phiên review — 2026-10-13

Nguyên nhân: bản review learning-flow trong plans/reports/review-261013-1855-roadmap-learning-flow-and-gaps.md. Người dùng chọn giữ testcontainers (RV1 phương án O2), dùng thứ tự xây dựng 1 → 3 → 2 → 4 → 5 (RV7) và thêm một phase đánh giá (RV9 phương án O2). Tất cả RV1–RV9 đã được áp dụng.

#Thay đổiỞ đâu
RV1main_test.go của từng package khởi động hoặc tái sử dụng một container testcontainers có tên và tạo database riêng; HANGO_TEST_DB_DSN là một override tùy chọn cho CI; không có testing.go ngoài test, nên binary production không link testcontainersphase 2
RV2Worker dựng scope từ role và trạng thái hiện tại của creator và làm run thất bại với creator_inactive khi creator bị vô hiệu hóa hoặc không còn; role rỗng là không hợp lệ và visibility(scope) fail closed với nóphase 2, 4, 5
RV3Việc tạo tenant, bootstrap và các route nền tảng tác động lên một tenant đều ghi bên trong scope của tenant đó, nên system pool giữ đúng năm nơi gọiphase 2, 16
RV4Phase 11 phụ thuộc [9, 10]phase 11, plan.md
RV5internal/run/reconcile.go và test của nó chuyển từ Modify sang Createphase 5
RV6Mười chỗ văn bản lỗi thời được sửadocs/roadmap.md, phase 6, 11, 12, 13, 14
RV7Thêm mục Build order; “Next up” của trang đi theo nóplan.md, docs/roadmap.md, build_plan_html.go, plan-template.html
RV8Các checkpoint review 5a và 5b bên trong phase 5phase 5
RV9Phase 17, vòng đánh giá, 8h + 2h, không thêm packagephase 17, plan.md, docs/roadmap.md

Effort ròng: 221h → 231h (+10h, toàn bộ là phase 17). Cài đặt 186h → 194h, tài liệu học 35h → 37h.

Phiên quyết định — 2026-10-14

Nguyên nhân: lần deploy đầu tiên dành cho gia đình của owner, và bản so sánh trong plans/reports/research-261014-1634-aicoworker-goclaw-hango-comparison.md. Người dùng chọn một kênh Telegram cho gia đình và một runtime Claude Code CLI giới hạn cho owner. Web chat để sau. Các request giả dạng Claude Code bằng subscription OAuth token vẫn nằm ngoài phạm vi.

#Thay đổiỞ đâu
DS1Phase 18, kênh Telegram: long polling, chat riêng tư, bản đồ Telegram-sang-user hango tĩnh, trả lời dạng stream bằng cách sửa message, tạo run dùng chung với handler HTTPphase 18, plan.md, docs/roadmap.md
DS2Phase 19, runtime Claude Code CLI: claude -p làm tiến trình con cho các agent có provider = "claude_cli", kiểm tra chỉ-owner khi lưu agent, tạo run và claim, tool qua một MCP bridge loopback, usage bị loại khỏi quota tenantphase 19, plan.md, docs/roadmap.md
DS3Giới hạn package 18 → 20 cộng internal/tool/builtin (internal/telegram, internal/claudecli)plan.md
DS4Build order đặt 18 và 19 sau 13plan.md, docs/roadmap.md
DS5MCP bridge của phase 19 được viết tay bằng thư viện chuẩn; không thêm SDK Go chính thứcphase 19
DS6Phase 14 phụ thuộc 18 và 19 và thêm một dashboard hango-channels cùng ba alert cho Telegram và Claude CLIphase 14, plan.md
DS7Phase 18 thêm trả lời Markdown-sang-HTML với phương án dự phòng plain text, dự phòng khi sửa message lỗi, retry_after trên mọi lời gọi, back-off khi 409 với poll watchdog, đăng ký lệnh, typing lặp lại và ngữ cảnh reply-to (+5h)phase 18
DS8Phase 20 đưa kênh Telegram tiến gần goclaw và OpenClaw: ảnh, album, tài liệu, voice note có phiên âm, gửi file ra, debounce, pairing code, chat nhóm có mention gating, nút approval và reaction trạng thái; nó thêm provider image block và migration 00012, và chạy sau phase 16phase 20, plan.md, docs/roadmap.md
DS9LLM gateway với quota theo từng user là một follow-up; cả gia đình dùng chung một quota tenant cho đến lúc đóplan.md
DS10Nhóm của phase 20 theo goclaw: pairing nhóm, một group principal và một session dùng chung cho mỗi nhóm hoặc forum topic, policy open/allowlist/disabled, require_mention với chế độ strict/yield, wake word, lịch sử nhóm đang chờ, writer, và gộp config môi trường → nhóm → topic; forum topic chuyển từ follow-up vào phase 20 (+5h)phase 20, plan.md, docs/roadmap.md
DS11Phiên âm giọng nói dùng bất kỳ endpoint tương thích OpenAI nào (OpenAI hoặc Groq Whisper) với gợi ý HANGO_STT_LANGUAGE, vi cho gia đình, transcript được chuẩn hóa NFC và một test fixture tiếng Việt có cổng điều kiện (+1h)phase 20
DS12Chat nhóm bị hoãn: mỗi người dùng chat riêng tư trong lúc này, và thiết kế nhóm của goclaw chuyển sang một mục hoãn trong phase 20 với HANGO_TELEGRAM_GROUP_HISTORY_LIMIT (mặc định 50, 0 là tắt) ghi đè được theo từng nhóm và topic; câu hỏi về group principal vẫn mở cho đến khi nhóm quay lại (−9h)phase 20, phase 18, plan.md, docs/roadmap.md
DS13Tầng HTTP dùng http.ServeMux của thư viện chuẩn với một router nhỏ trong internal/server (Handle, Group, một bộ chuyển đổi lỗi, DecodeJSON, một writer ghi lại status) thay cho Echo v5; SetAuditAction nhận *http.Request, và tên span cùng http.route lấy từ r.Pattern (+1h)phase 1, phase 2, plan.md, docs/tech-stack.md, docs/roadmap.md
DS14Web chat và một trang web admin là các follow-up xây bằng Svelte 5 trên HTTP API hiện có; không cái nào là phase của v1, nên effort không đổiplan.md, docs/roadmap.md, docs/tech-stack.md

Effort ròng: 231h → 290h (+59h). Cài đặt 194h → 246h, tài liệu học 37h → 44h.

Phiên quyết định — 2026-10-15

Nguyên nhân: bảng “Optimize context” của AICoworker, được ghi là A4 trong plans/reports/research-261014-1634-aicoworker-goclaw-hango-comparison.md.

#Thay đổiỞ đâu
DS15Context inspector là một follow-up làm sau trang web admin, không phải phase của v1. Spec của nó, gồm cả thay đổi usage_records cho các lệnh gọi thử, nằm trong followup-context-inspector.md; effort v1, thứ tự build và số migration không đổiplan.md, followup-context-inspector.md, docs/roadmap.md