Vấn đề không nằm ở kế hoạch, mà ở chỗ kế hoạch không có gì kiểm chứng tự động. Ba bằng chứng trong hôm nay:
packages/extractor/src/dates.ts:96 có một dấu } thừa → 5 trong 10 file test không compile được, chỉ 16 test chạy. Phát hiện lúc 18/09 16:26 khi bắt đầu bật typecheck; đã sửa..github/, và package.json root chỉ có test (chạy mỗi extractor). Đây là lý do trực tiếp của lỗi trên..gitignore không chặnLuật cũ chỉ khớp results.*.json, không khớp results-deepseek.log / results-gemini.log — hai file này có thể chứa nguyên văn tin của khách. Đã chặn và kiểm chứng bằng git check-ignore -v.git grep ADR-006 (không file code nào nhắc) cho 0 kết quả; conversationStore.ts và whatsapp.ts vẫn ghi "no ADR yet". Tài liệu không có trong git thì không tồn tại — và nó còn đảo ngược ADR-005a mà không có dòng Supersedes.Không thể lập kế hoạch trên một cây nguồn chưa đóng băng. Hiện có 16 file modified + 5 file test mới chưa commit; main còn ahead 2 chưa push.
npm run verify.main + deploy rồi test 1 tin thậtDeploy là CLI từ repo root (vercel build → vercel deploy --prebuilt --prod), không phải push-to-deploy. Phải xác nhận webhook hết 404 và prod đã có WHATSAPP_*.WHATSAPP_TURN_TIMEOUT_MS và WHATSAPP_TIMEOUT_MS đã dùng trong code nhưng chưa có trong tài liệu bàn giao.Làn này không thêm tính năng nào cho khách. Nó tồn tại để sự việc ở mục 0 không lặp lại. Trạng thái 18/09: 3/5 việc đã xong.
.github/workflows/ci.yml — npm ci rồi npm run verify, Node 20, cancel-in-progress. Còn phải kiểm chứng bằng một lần push thật.npm run typecheck chạy cả 2 workspace (--workspaces --if-present); npm run verify = typecheck + test. Bằng chứng: npm run verify exit 0, 11/11 file test pass (78 test) sau khi thêm test/questions.test.ts.results*.log và results-*.json. Kiểm chứng: git check-ignore -v trỏ đúng về dòng 13 của .gitignore.tsconfig.json của extractor chỉ include ["src"] — 5 file test mới không được kiểm kiểu. Đây là lỗ hổng còn lại của L1..agents/Đang untracked nhưng chứa skill dùng chung (browser-skill, technext-status-report). Hoặc commit, hoặc ghi vào .gitignore — không để nó hiện mãi trong mỗi lần git status.Ba lỗi trong làn này nằm trong code đang chạy, không phải nâng cấp: chúng đã có mặt trên main hôm nay.
conversationStore.ts dùng ttlMs = THREAD_TTL_MS cho khoá claim của wamid. Nếu function bị kill sau khi claim mà trước khi releaseMessage, tin đó bị khoá 24 giờ — khách im lặng mất một ngày. Cần: claim in-flight ~60s + một "done marker" riêng có TTL dài.releaseMessage không có token → double replyHiện release chỉ theo id, nên một lượt retry cũ có thể nhả claim của lượt mới. Cần claimMessage → {fenceToken} và releaseMessage(id, token) kiểm token. Bằng chứng phải là test: 3 lần giao cùng một wamid ⇒ đúng 1 reply.thread-lock:<phone> + gộp tin trong 2–3 giây thành một lượt. Cập nhật 19/09: đúng ca này đã xảy ra trong lần chạy thật — khách gửi tin thứ hai (“hi”) cách tin đầu 1 giây và nhận một câu trả lời riêng, lặp lại nội dung tin trước. Chưa sửa. Cập nhật 20/09: truy lại đúng race trong code — app.ts xử lý mỗi webhook độc lập (store.append → store.history → converse(), mất 3–6s), không có mutex/queue theo số điện thoại nào cả (grep lock/mutex/debounce/queue trong apps/casa-bff/src: không có kết quả). Kịch bản cụ thể: webhook A đọc history lúc chỉ có tin 1, đang "nghĩ"; tin 2 đến, webhook B ghi vào rồi đọc history đủ cả hai; A trả lời xong trước dựa trên history thiếu tin 2 → hỏi lại đúng thứ khách vừa nói ở tin 2 → khách nhận 2 tin, tin đầu hỏi thừa. Đề xuất rẻ nhất: debounce ~1.5–2s theo từng số điện thoại trước khi gọi converse() — nếu có tin mới đến trong cửa sổ đó thì gộp vào cùng một lượt thay vì gọi model riêng. Chưa làm — cần xác nhận trước vì đổi timing đua với cửa sổ redelivery 20s của Meta.fallbackReply("apology", lang) trong questions.ts — provider lỗi/quá hạn thì khách nhận một câu xin lỗi tĩnh (en/vi/zh theo chính lời khách) và thread được đánh dấu cần người thật. Claim được giữ lại vì khách đã nghe được gì đó, nên redelivery của Meta là duplicates: 1 chứ không thành tin thứ hai. Bằng chứng: whatsapp.test.ts — "apologizes rather than falling silent…" (gateway 500 hai lần ⇒ replied: 1, failed: 1, handoffs: 1, history có câu xin lỗi) và test deadline (turn quá hạn ⇒ vẫn đúng 1 tin gửi đi).paused + wantsHuman() + ASK_LIMIT=8 được đọc trước khi gọi model (app.ts), lưu trong conversationStore (pause/resume/needsTelling/markTold). Khách được nói một lần, không lặp lại câu giữ chỗ ở mỗi tin tiếp theo (HOLD_REPEAT_MS = 10'). Lễ tân đọc và trả thread bằng npm run whatsapp:threads, hoặc GET /v1/channels/whatsapp/threads + POST /v1/channels/whatsapp/threads/:phone/resume (khoá bằng x-verify-token). Test: thread đã pause ⇒ provider.call không tăng; từ khoá escalate tiếng Việt ⇒ 1 tin giữ chỗ tiếng Việt, 0 lần gọi model; quá hạn mức hỏi ⇒ reason: asking_limit.packages/extractor/test/conversationStore.contract.test.ts — một spec 16 test chạy trên cả hai adapter (in-memory + Redis REST) qua bảng ADAPTERS, thêm adapter mới chỉ cần một dòng. Bao phủ thứ tự lượt, cap MAX_TURNS, claim NX + fenceToken, hết hạn in-flight 60s nhưng "done" giữ 24h, park/list/resume, needsTelling theo cửa sổ lặp, clear(), phone lock (kể cả khi work ném lỗi), TTL thread 24h. Lỗi thật tìm được khi viết spec: mock KV cũ trong redisStore.test.ts trả "OK" cho mọi lệnh nó không biết và không xử lý SADD/SMEMBERS/SREM — nên đường pause/resume/giữ chỗ chưa từng được test trên Redis, đúng loại lệch chỉ hiện ở production. Mock mới ném lỗi khi gặp lệnh lạ. Còn lại: chốt backend chính thức (Upstash/Vercel KV hay Postgres) — hiện chọn theo env KV_REST_API_URL, production chưa cấu hình nên vẫn in-memory.docs/adr/ADR-006-reply-contract.md, status proposed (chờ Anthony ký), có dòng Supersedes ghi rõ clause cap 8 lượt của ADR-005a đã bị thay bằng cap theo kích thước. Nội dung: một tin mỗi lượt do code viết, 3 loại reply, house-norm chỉ hiện chứ không hỏi, không suy diễn field ảnh hưởng tiền, evidence + language chỉ đọc lời khách.Sau L2, kênh đã "cứng". L3 làm cho câu trả lời đúng và tất định — phần duy nhất khách thực sự nhìn thấy.
READY_FIELDS khỏi danh sách hỏidone = questions.length === 0 chỉ phủ 7 field trong PRIORITY. WIP vừa thêm 5 field optional (transportType, diver, guestType, diveFrom, diveTo) mà không field nào được hỏi ⇒ done:true vẫn có thể xảy ra khi chưa biết roundtrip. Điều kiện bàn giao phải tách khỏi điều kiện hỏi. Cập nhật 18/09: diveFrom, diveTo và transportType nay được hỏi khi áp dụng được ⇒ lỗ "chưa biết roundtrip mà vẫn done:true" đã hẹp lại. Cập nhật 18/09 (lần 2): diver nay đã nằm trong danh sách hỏi (rule mới trong questions.ts, không còn suy từ regex), nên chỉ còn guestType là field optional không bao giờ được hỏi — READY_FIELDS vẫn phải tách. Cập nhật 24/09: đã tách. questions.ts có HANDOFF_REQUIRED_FIELDS (13 field), NEVER_ASKED_FIELDS (kèm nguồn giá trị: checkOut do code suy ra, guestType do người thật xác nhận) và isReadyForHandoff(trip); converse.ts dùng nó cho done. Danh sách hỏi và điều kiện bàn giao cùng đọc từ một hàm openRules() nên không thể mâu thuẫn. Lỗi thật đã mắc và bị test bắt: bản đầu coi 3 field lặn là bắt buộc vô điều kiện, trong khi rule của chúng bị gate bởi diver === true và diveNotes vắng mặt — nên mọi khách không lặn và mọi ca lịch lặn chia ngày (đúng ca mà NEVER-RE-ASK sinh ra để phục vụ) không bao giờ bàn giao được. Sửa: field bắt buộc chỉ bị kiểm khi rule của nó áp dụng cho chuyến này. Test: 4 test mới trong questions.test.ts, gồm test bất biến "mọi field bắt buộc phải được hỏi, hoặc do code điền, hoặc có lý do never-ask".transportType đang bị suy diễnextract.ts tự suy roundtrip từ dữ liệu khác trong khi giá phụ thuộc vào nó. ADR-006 Decision 4 (mơ hồ ⇒ missing và hỏi lại) là cách sửa đúng — phải làm trước demo. Cập nhật 18/09: phần diver của lỗi tiền đã sửa — regex isDiving và dive window suy diễn đã bị xoá, diver nay là câu hỏi và cửa sổ lặn chỉ hỏi sau khi khách xác nhận. Riêng transportType vẫn còn derived + hỏi lại khách, tức là chữa triệu chứng — giá vẫn đang phụ thuộc một giá trị do code tự đoán. Cập nhật 19/09 (chạy thật trên điện thoại): provider có lúc bỏ hẳn key diver khỏi tool arguments — cùng một tin của khách mà lượt này hỏi “có lặn không”, lượt sau không hỏi nữa, không lỗi, không log. Đã sửa ở extract.ts: key vắng mặt, cũng như state chỉ code được đặt (default/derived), bị quy về missing ⇒ thành câu hỏi; riêng 3 field lặn chỉ stated mới sống sót. Bằng chứng: extract.test.ts 3 test mới + questions.test.ts 1 test cho key vắng mặt (npm run verify 108/108). Cập nhật 24/09: hết suy diễn. postProcess không còn ghi roundtrip. Chỉ còn một trường hợp code được phép điền: khách từ chối transfer ⇒ none, derived (không cần hỏi gì). Khách cần transfer mà chưa nói một chiều hay khứ hồi ⇒ để {value:null, state:"missing"}, câu hỏi trong questions.ts nổ ra, và giá lấy từ câu trả lời của khách. Test: extract.test.ts 2 test viết lại, questions.test.ts 1 test viết lại, evalReplay cập nhật theo hành vi mới.assumed cho default ảnh hưởng tiềnguestType mặc định retail và transportType hiện là default im lặng — nhưng agent được giảm 30%, nên một default sai là một báo giá sai. Phải hiển thị cho người thật, không nói với khách.converse.ts không còn gửi questions[0]; renderFormMessage() đã được thay bằng renderReply() với ba câu tất định (ADR-006). Khách chưa nói gì ⇒ greeting (giới thiệu Casa, xin ngày nhận phòng / số đêm / số khách / tên). Đã hiểu một phần ⇒ xác nhận đúng những gì khách nói rồi hỏi nốt theo thứ tự ưu tiên. Hết mục cần hỏi ⇒ summary in ra từng giá trị để khách kiểm. House-norm (phòng / bữa ăn / đưa đón) không còn bị hỏi (BASELINE = ["missing"]) mà hiện trong summary kèm nhãn (assumed). PRIORITY đổi thành RULES có điều kiện when: diveFrom/diveTo chỉ hỏi khi khách có lặn, transportType chỉ hỏi khi khách cần đưa đón. Bằng chứng: questions.test.ts (9 test: 3 loại reply, cờ (assumed), ngày theo en/vi/zh) + converse.test.ts (6 test) + whatsapp.test.ts (1 tin khách ⇒ 1 tin WhatsApp) + normalize.test.ts (7 test) · npm run verify 12/12 file, 91/91 test. Đã bù 18/09 (L2): câu xin lỗi tĩnh + cờ paused — khách không còn nhận không gì cả.docs/adr/ADR-006-reply-contract.md. Model không viết câu nào cho khách; renderReply() trong questions.ts chọn một trong ba câu tất định (greeting / questions / summary) và trả kèm replyKind cho BFF. Vì không có model nào viết câu trả lời nên nó không thể cam kết giá/phòng: câu cuối nói rõ “nothing is booked yet” rồi mời người thật xác nhận. Bằng chứng: test/questions.test.ts (9 test: 3 loại reply, nhãn (assumed), ngày theo en/vi/zh).WEEKDAYS trong dates.ts có đủ dạng số (thứ 2…thứ 7 và thu 2…thu 7) bên cạnh dạng chữ. Đây là lỗi thật: khách trả lời “thứ 7 tuần sau” trước đó resolve ra null ⇒ mất ngày nhận phòng. Test dùng TODAY cố định 2026-09-15, không dùng ngày thật: dates.test.ts (10 test) — “thứ 7 tuần sau” ⇒ 2026-09-26.Không việc nào dưới đây làm được trước khi có đầu vào tương ứng. Danh sách này thay cho các mục "Open Items" đã lỗi thời trong sprint tracker.
schema.ts tự ghi là PLACEHOLDER. Mọi endpoint mới dựa trên nó — xây trên placeholder là xây trên cát.Bản 17/09 liệt kê việc nhưng không nói thế nào là xong, nên "xong" trôi theo trí nhớ. Một việc chỉ được coi là Done khi có cả 4:
mainKhông phải "đã viết trong working tree". Nếu chưa commit, nó không tồn tại với người còn lại.npm run verify exit 0 + 10/10 test file pass.D5:D15 ⇒ thêm dòng mới sẽ không được đếm, phải sửa formula trước.Roadmap cũ chỉ liệt kê việc sẽ làm, nên không ai thấy việc gì đang bị hoãn ầm thầm. Ghi rõ ra:
MAX_TRANSCRIPT_CHARS = 60_000, giữ đầu + đuôi) vẫn giữ được dữ kiện. Chỉ làm sau khi có số đo ở L3./v1/estimate/compute, /v1/booking/submit, draft store… đều chờ Phillip freeze schema. Xây trước là tự tạo nợ phải viết lại.Thứ tự này là thứ tự phụ thuộc, không phải thứ tự sở thích: L0 trước L1 vì không có cây nguồn đóng băng thì CI chỉ báo đỏ vì việc của người khác; L2 trước L3 vì cứng kênh rẻ hơn và chặn rủi ro lớn hơn.
transportType + default assumed + dạng ngày Việt + golden test Anh Minh. Ra được: done có nghĩa, không còn đường nào để bịa giá.derived/defaultĐúng cơ chế đã gây lỗi roundtrip. Chặn bằng trạng thái assumed + luật renderer + case eval để ngưỡng "fabricated = 0" bắt được hồi quy.WHATSAPP_* ⇒ webhook 404Đây là blocker cứng của mọi demo qua WhatsApp. Chặn bằng L0.2.