3 agent, 2 session, và 1 cái bug không nhớ đã fix hay chưa
Cách tôi dùng Beads (cái task tracker dạng đồ thị) để dẹp nạn 'lội chat', và khi nào mới cần thêm 'bộ nhớ' cho agent. Kinh nghiệm thật, giới hạn thật.
Mục lục
Tối thứ Ba. Tôi lướt chat tới mỏi tay mà vẫn không biết task B đã xong chưa hay mới chỉ ‘chém gió’ về nó. Ba session agent khác nhau đã đụng vào codebase này. Một đứa chắc chắn đã fix một cái bug. Và tôi khá chắc là hôm nay mình lại fix đúng cái bug đó lần nữa. Đây không phải là pro, mà là mớ hỗn độn núp bóng ‘context’.
Tôi cần biết việc gì tiếp theo một cách máy móc, chứ không phải một context window to hơn. Thế là tôi tìm thấy Beads.
Hai kiểu ‘não cá vàng’ của agent (và tại sao chúng khác nhau)
Gộp task tracking với memory retrieval nghe có vẻ hay cho đến khi bạn bắt tay vào làm. Rồi bạn sẽ nhận ra chúng giải quyết hai vấn đề khác nhau:
| Kiểu ‘quên’ | Cái gì toang | Ví dụ thực tế | Cái gì cứu |
|---|---|---|---|
| Quên quy trình | Không biết task nào ready, bị block, hay đã close | Refactor auth qua 3 session; session 2 đã close token handler; session 3 vẫn hăm hở làm lại | Beads—đồ thị có cấu trúc với bd ready, dependency |
| Quên ngữ cảnh | Không nhớ tại sao code lại viết như vậy | Mở một file helper validation vài tháng sau; quên mất nó viết ra để chống race condition | Nội dung issue, commit message của Git, ADRs—và các tool retrieval nếu bạn chịu khó nối dây |
Tôi đã từng coi chúng là một. Sai lầm. Beads là cách tôi ngừng biến “kế hoạch” thành một cuộc khảo cổ trong Slack. ADRs và Git là cách tôi ngừng học lại từ đầu lý do một đoạn code tồn tại.
Beads (bd) là cái issue tracker dạng đồ thị chạy trên Dolt mà tôi luôn dùng đầu tiên. State nằm trong
.beads/. Tài liệu ở gastownhall.github.io/beads. Nó vẫn không trả lời được tại sao code lại trông như vậy khi tôi mở lại file vài tháng sau, nên tôi mới chồng thêm các tool retrieval (MCP/SDK) lên trên. Đây là kinh nghiệm thực tế, không có chém gió về một job tôi chưa từng làm.
Beads lưu gì (fact, không phải feeling)
- Engine: Dolt—dữ liệu có cấu trúc, có version; mặc định nằm trong
.beads/embeddeddolt/. - IDs: ID dạng hash (ví dụ
bd-a1b2) để các branch và agent ít đụng hàng nhau hơn. - Graph links:
relates_to,duplicates,supersedes—những thứ khiếnbd readythực sự hoạt động.
Đây là cách chúng nó nói chuyện với nhau:
View diagram source
graph LR
Agent["Agent / Developer"]
Beads["Beads<br/>(Dolt state)"]
Git["Git / Remote"]
Agent -->|"bd create/update"| Beads
Beads -->|"auto-commit"| Git
Beads -->|"bd ready --json"| Agent
Agent -->|"bd dolt push"| Git
Git -->|"bd dolt pull"| Beads
style Beads fill:#1e1e2e,stroke:#84ffff,color:#ecfdf5
style Git fill:#1e1e2e,stroke:#84ffff,color:#ecfdf5
style Agent fill:#181825,stroke:#84ffff,color:#ecfdf5 Chỗ này dễ toang: Nếu bạn không bao giờ bd close một task, bd ready sẽ thành truyện viễn tưởng. Tôi đã tự lừa mình kiểu này nhiều lần rồi. Database không tha thứ đâu.
Một buổi sáng với Beads (trước và sau)
Không có bd ready (kiểu cũ):
09:00: Lội chat. “Tụi mình xong cái token handler chưa nhỉ?” Lội ngược dòng thời gian về session hôm qua. Có vẻ xong rồi. Session 1 nhắc tới. Session 2 bảo “trông ổn.” Session 3 (hôm nay) lại thấy tôi giải thích lại logic validation tôi viết ở session 1. Tốn 30 phút chỉ để load lại context.
09:35: Bắt đầu code. Sửa. Nửa chừng nhận ra: cái này làm rồi mà. Undo. Xin lỗi con model. Phí thời gian.
Với bd ready (kiểu đúng):
09:00: Mở terminal.
$ bd ready --json
[
{
"id": "bd-7f2a",
"title": "Add refresh-token rotation",
"status": "ready",
"claimed_by": null,
"blockers": []
}
]
09:01: Chính xác một việc. “Hốt” nó ngay.
$ bd update bd-7f2a --claim --json
{
"id": "bd-7f2a",
"claimed_by": "agent-session-3",
"updated_at": "2026-04-10T09:01:22Z"
}
09:02: Check memory nếu cần. Code. Đóng task.
$ bd close bd-7f2a --reason "refresh-token rotation complete; added Redis entry expiry" --json
{
"id": "bd-7f2a",
"status": "closed",
"closed_at": "2026-04-10T09:35:12Z"
}
10:00: Session 4 khởi động. Check bd ready. Task kia không xuất hiện vì đã được close. Không làm lại việc. Không khảo cổ.
View diagram source
graph TD
Start["BẮT ĐẦU: Mở session"]
Query["bd ready --json"]
Empty{Trống?}
Done["Tạm xong<br/>hoặc tạo epic mới"]
Pick["Chọn task ưu tiên"]
Claim["bd update --claim"]
Work["Code + test + note"]
Close["bd close --reason"]
Push["bd dolt push<br/>(tùy chọn)"]
End["KẾT THÚC"]
Start --> Query
Query --> Empty
Empty -->|Yes| Done
Empty -->|No| Pick
Pick --> Claim
Claim --> Work
Work --> Close
Close --> Push
Push --> End
style Start fill:#1e1e2e,stroke:#84ffff,color:#ecfdf5
style End fill:#1e1e2e,stroke:#84ffff,color:#ecfdf5
style Claim fill:#181825,stroke:#84ffff,color:#ecfdf5
style Close fill:#181825,stroke:#84ffff,color:#ecfdf5 Tài liệu AGENT_INSTRUCTIONS.md đã nói rõ: không dùng bd edit tương tác khi tự động hóa. Hãy dùng bd update với flag hoặc stdin. Đó là giao kèo với chính bạn trong tương lai.
Bài học xương máu #1: Khi cái task graph ‘nói láo’
Tôi đã mất 45 phút để làm lại một bug token-rotation đã được fix. Lý do: Tôi claim task trong session 1, làm xong, nhưng quên close. Session 2 thấy nó vẫn được claim. Session 3 (hôm nay) cũng thấy vậy và nghĩ nó chưa xong. Làm lại từ đầu. Nửa chừng mới nhận ra. Lãng phí thời gian lội chat để tìm xem mình đã close nó ở đâu (thực ra là chưa).
Bài học: bd close --reason "..." không phải để cho đẹp. Đó là giao kèo với bạn-trong-tương-lai, rằng “cái này đã xong, đây là lý do.” Bỏ qua nó, và task graph của bạn sẽ trở thành một kẻ nói dối.
Lý do close tốt:
bd close bd-7f2a --reason "token rotation xong; Redis TTL đã sync với JWT exp; thêm test trong auth.spec.ts"
Lý do close tồi:
bd close bd-7f2a --reason "xong"
Tại sao tôi lại thêm ‘bộ nhớ’ ngữ cảnh?
Beads trả lời việc gì tiếp theo. Nó không tự động giải thích tại sao code lại như vậy, trừ khi bạn đã ghi nó ra đâu đó mà agent có thể query. Vì vậy, tôi coi Git + ADRs là chân lý, và khi nào thấy ‘đau’ quá, tôi mới thêm một kênh truy xuất—MCP hoặc memory tự host—để tìm kiếm note ngắn hoặc đoạn code.
Đây là cây quyết định tôi hay dùng:
View diagram source
graph TD
Q1["Project chạy<br/>dưới 1 tháng?"]
Q2["Đã tự hỏi 'sao code viết vầy'<br/>hơn 2 lần?"]
Skip["Tạm thời<br/>chưa cần memory"]
Add["Thêm một lớp MCP<br/>hoặc memory"]
Q1 -->|Yes| Skip
Q1 -->|No| Q2
Q2 -->|Yes| Add
Q2 -->|No| Skip
style Skip fill:#1e1e2e,stroke:#84ffff,color:#ecfdf5
style Add fill:#181825,stroke:#84ffff,color:#ecfdf5 Format một memory entry ‘tốt’:
beads_id: bd-7f2a
title: Triển khai xoay vòng token
paths: src/auth/token-handler.ts, src/cache/redis.ts
decisions:
- Refresh token TTL = 7 ngày (yêu cầu business, hết hạn trước JWT)
- Redis entry tự hết hạn (dùng SET với flag EX, không dọn dẹp thủ công)
- Logout sẽ xóa cả JWT và refresh token
references: [OWASP session management](https://cheatsheetseries.owasp.org/), #bd-6c1f (bug token cũ)
Format một memory entry ‘tồi’:
tụi mình làm cái token và nó chạy rồi, học được về redis ttl
với mấy cái owasp, và mình nghĩ nên xem lại luồng auth
vì chắc còn bug ở đó
Cái đầu tiên tìm kiếm được. Cái thứ hai là rác.
View diagram source
graph LR
Session["Agent session"]
Beads["Beads<br/>việc gì ready?"]
Memory["MCP / Memory<br/>nhớ lõm bõm"]
Git["Git + ADRs<br/>chân lý đã merge"]
Session --> Beads
Session --> Memory
Session --> Git
Beads -.->|có cấu trúc| Git
Memory -.->|gần đúng| Git
style Beads fill:#1e1e2e,stroke:#84ffff,color:#ecfdf5
style Memory fill:#1e1e2e,stroke:#84ffff,color:#ecfdf5
style Git fill:#181825,stroke:#84ffff,color:#ecfdf5 Retrieval là gần đúng. bd ready là có cấu trúc. Tôi phải nhớ rõ hai khái niệm này, nếu không sẽ kỳ vọng sai.
Bài học xương máu #2: ‘Bộ nhớ’ Rube Goldberg
Tôi từng bắt đầu với ba server MCP: một để search code, một cho memory, và một để cào changelog. Cái giá phải trả để giữ chúng đồng bộ còn lớn hơn lợi ích. Sau hai tuần, tôi phải ghi memory vào hai nơi và xác minh ở nơi thứ ba. Bỏ cuộc vào tuần thứ 3.
Checklist trước khi thêm MCP thứ hai:
- MCP đầu tiên có thực sự được gọi không (check log)?
- Kết quả của nó có giúp ra quyết định tốt hơn
git logkhông? - MCP mới có bị trùng lặp chức năng không (ví dụ: đừng thêm MCP changelog nếu
git logđã đủ)? - Tôi có thể quản lý config server trong
AGENTS.md(tên tool thật, không phải chém gió)?
Quy tắc của tôi bây giờ: một MCP memory, một MCP code, hết. Nếu có tranh chấp, Git thắng.
Pattern AGENTS.md (cách biến tool thành ‘vũ khí’)
Tôi đã sửa cái này ba lần. Version 1 có tên tool memory không khớp với server, agent không bao giờ gọi. Version 2 thêm đúng tên nhưng thiếu ngữ cảnh. Version 3 thêm quy trình và điều kiện. Đây là v3:
## Nguồn chân lý về task
- Trước khi làm: `bd ready --json`.
- Nhận task: `bd update <id> --claim --json`.
- Hoàn thành: `bd close <id> --reason "..." --json`.
- Không bao giờ dùng `bd edit`; chỉ dùng `bd update` + flag.
## Nhớ lại (tên tool MCP thật từ `mcp list`)
- Sau khi nhận task: `memory_search` với tiêu đề issue, tên epic, từ khóa.
- Nếu sửa một path: `code_search` cho path hoặc symbol đó.
- Nếu không tìm thấy gì: dùng `git log -- path` và `bd show <id>`.
## Ghi lại (ngắn + có link)
- Khi close: ghi chú ngắn với `beads_id`, các path đã sửa, quyết định, và các việc cần làm tiếp.
- Giữ entry dưới 300 từ (dài hơn là thành rác).
## Quy trình (không bao giờ bỏ bước)
1. bd ready
2. claim
3. nhớ lại (nếu lần đầu đụng đến epic này)
4. code
5. close + ghi memory
6. tùy chọn: bd dolt push
Bài học cốt lõi: Nếu tool không có tên trong AGENTS.md, model sẽ không gọi chúng ngay cả khi chúng tồn tại. Tôi đã chứng kiến điều này. Tool có đó. MCP đang chạy. Model có quyền. Nhưng không có tên tool trong context mà model đọc được, nó sẽ bị “ngáo” và quay lại chat.
Một session thực tế trông như thế nào (template, không phải hư cấu)
Bước 1: Check task ready
$ bd ready --json | jq '.' | head -5
[
{
"id": "bd-7f2a",
"title": "Add refresh-token rotation",
"status": "ready",
"blockers": []
}
]
Bước 2: Nhận task
$ bd update bd-7f2a --claim --json
{
"id": "bd-7f2a",
"claimed_by": "agent-session",
"updated_at": "2026-04-10T09:01:22Z"
}
Bước 3: ‘Nhớ’ lại (nếu cần)
$ memory_search "token rotation refresh jwt"
[
{
"beads_id": "bd-6c1f",
"title": "Previous session token bug (expired JWTs not cleaned)",
"excerpt": "Refresh token TTL = 7 days; set Redis TTL with EX flag..."
}
]
Bước 4: Code (công việc chính)
Bước 5: Close và ghi lý do
$ bd close bd-7f2a --reason "token rotation complete; added Redis TTL sync; tests passing" --json
{
"id": "bd-7f2a",
"status": "closed",
"closed_at": "2026-04-10T09:35:12Z"
}
Bước 6: Ghi memory (nếu không phải task vặt)
beads_id: bd-7f2a
title: Triển khai xoay vòng refresh token
paths: src/auth/token-handler.ts, src/cache/redis.ts
decisions:
- Refresh TTL = 7 ngày (JWT + buffer)
- Redis SET với flag EX (tự dọn, không cần cronjob)
- Logout xóa cả hai token
references: bd-6c1f (bug dọn token cũ)
Cách ‘làm quen’ lại một file cũ
Session 4 mở một feature cũ. Đây là quy trình:
Tìm trên Beads:
bd list --filter "epic:auth" --json
Xem chi tiết:
bd show bd-7f2a
Query memory với beads_id:
memory_search "beads_id:bd-7f2a"
Quay về chân lý Git:
git log --oneline -- src/auth/token-handler.ts
Tạo một issue nhỏ với các gợi ý (đoạn memory, path:line), không phải một cục “sửa login” khổng lồ.
Những cú ‘tự hủy’ tôi đã trải qua
- Ném cả repo vào memory: Đã thử. Kết quả search trả về một mớ rác. Bây giờ tôi chỉ ghi các gạch đầu dòng theo issue với
beads_id+ paths. Tốn thêm 30 giây, nhưng giá trị gấp 10 lần. - Bỏ qua
bd close: Từng quên close một task.bd readyđã ‘nói láo’ với session tiếp theo. Phí mất một giờ. Không bao giờ nữa. - Dùng cả Jira + Beads + markdown: Ba nguồn chân lý = không có nguồn nào. Chọn một. Tôi chọn Beads.
- Coi embedding là chân lý tuyệt đối: Retrieval chỉ là một giả thuyết. Git + ADRs mới là bằng chứng. Luôn kiểm tra lại.
- Ghi memory dài hơn 300 từ: Vô nghĩa. Nếu nó phức tạp đến vậy, hãy tạo một issue Beads mới.
Các cột mốc (để biết bạn đang đi đúng hướng)
| Tuần | Dấu hiệu |
|---|---|
| 1 | Mỗi session bắt đầu bằng bd ready --json; mỗi task xong đều bd close với lý do rõ ràng |
| 2 | AGENTS.md ghi tên tool MCP thật (không phải placeholder); ‘nhớ’ lại trước khi sửa lớn |
| 3 | Memory có beads_id + paths + quyết định (không phải văn lan man) |
| 4 | Mở lại feature cũ chỉ cần Beads + memory + Git; không cần giải thích lại từ đầu |
Ba chiêu ‘chôm’ ngay hôm nay
Chiêu 1: Bắt đầu với bd ready
Ngừng đoán mò ưu tiên từ chat. Hãy hỏi database. Nếu bd ready trống, bạn hoặc là đã xong việc, hoặc là chưa bao giờ mô hình hóa các blocker. Biết được là cái nào đã là một nửa trận chiến.
bd ready --json
Chiêu 2: Close với lý do
bd close <id> --reason "..." không phải để cho đẹp. Đó là giao kèo với bạn-trong-tương-lai. Lý do tốt là: cái gì đã thay đổi, tại sao nó thay đổi, tìm diff ở đâu. Hãy viết nó ra.
Chiêu 3: Một memory + một code search, không hơn
Nối chúng vào AGENTS.md với tên tool thật (không phải placeholder). Một MCP cho mỗi loại. Git thắng khi hòa. Coi retrieval là một giả thuyết, không phải luật lệ.
Nếu bạn không nhớ gì khác: hãy chạy bd ready trước khi bạn tranh cãi với model về các ưu tiên, và coi kết quả semantic là những giả thuyết bạn cần xác minh trong Git. Mọi thứ khác—thêm MCP, nén, các đồ thị màu mè—chỉ là gia vị. Tôi vẫn đang cân chỉnh xem việc ghi lại memory có đáng để bảo trì không. Câu trả lời thật lòng là: nó hữu ích khi bạn giữ các entry ngắn gọn và có liên kết ID.