# Mổ xẻ ClarkCant: một tin nhắn trở thành câu trả lời, widget hay task như thế nào

Đi một vòng kiến trúc ClarkCant từ source: một runtime nhiều client, conductor ra quyết định, Pi nằm sau một adapter, widget là bộ từ vựng, policy chỉ được siết lại, memory xóa được, và voice chỉ là giọng nói.

Duy Nguyen /zuey/ · 2026-10-05T10:09:10.065Z

Source: https://clarkcant.cc/vi/blog/clarkcant-architecture-deep-dive

Nhiều người hỏi tui bên dưới cái Orb thì ClarkCant thực ra là gì. Trả lời thật lòng: một runtime Node.js giữ một database SQLite, một agent loop mượn từ Pi, và rất nhiều luật về chuyện ai được quyết cái gì. Khung chat chỉ là phần bạn nhìn thấy.

Bài này đi qua kiến trúc theo cách code chạy, không theo cách slide gọi vốn kể. Tui sẽ theo một tin nhắn từ ô nhập tới câu trả lời, rồi nhìn vào những phần giữ cho chuyến đi đó an toàn: widget, policy, memory, voice, và mấy thứ session hay node mà bạn đáng ra không bao giờ phải để ý. Mọi đường dẫn file trong bài đều có thật. Chỗ nào mới thiết kế mà chưa build, tui nói rõ.

Có một luật trong DESIGN.md chi phối hết phần dưới: đừng bắt người dùng hiểu kiến trúc mới làm được việc. Viết vài nghìn chữ về kiến trúc dưới cái luật đó thì hơi buồn cười. Nhưng bạn là dân kỹ thuật, bạn hỏi thì tui kể.

## Một runtime, nhiều client

ClarkCant không phải app desktop kèm một process phụ. Ngay từ commit đầu, nó là một runtime mang đi đâu cũng chạy, với nhiều client hội thoại. apps/runtime là node headless: composition root, command gateway có xác thực qua HTTP và WebSocket, danh tính node, vòng đời và health. apps/web là client trình duyệt. apps/desktop là cái vỏ Electron mỏng, IPC có kiểu, bật contextIsolation, tắt nodeIntegration, và không tự chạy scheduler nào. Đem đúng runtime đó lên VPS, bỏ Electron đi, là có bản server.

Mỗi node có danh tính riêng, database SQLite riêng chạy WAL, credential riêng và thư mục riêng. Một cuộc hội thoại có đúng một home node. Các node có thể pair với nhau và giao việc có phạm vi qua NodeLink, nhưng không bao giờ dùng chung filesystem hay database đang chạy. Tài liệu kiến trúc nói hay hơn tui: sự liền mạch nằm ở giao tiếp, ủy quyền và cách hiển thị, chứ không nằm ở việc giấu hết khác biệt hay copy secret đi khắp nơi.

Runtime chạy thẳng TypeScript trên Node 22.19 trở lên, SQLite lấy từ node:sqlite có sẵn, nên phần lõi không cần native module nào. Cố ý đó. Ít thứ phải compile thì ít thứ phải audit.

Số liệu repo ClarkCant ngày 5/10/2026: 685 commit kể từ commit đầu ngày 16/9/2026; 5 app, 18 package và 6 pack chính chủ; trong docs/conformance-traceability.md, 10 trên 18 hạng mục phạm vi là PASS, 8 là PARTIAL, và cả 77 acceptance test (T01 tới T77) đều PASS.

```json
[
  {
    "id": "commits",
    "label": "Commit",
    "value": 685,
    "hint": "từ 16/9 tới 5/10/2026"
  },
  {
    "id": "apps",
    "label": "App",
    "value": 5
  },
  {
    "id": "packages",
    "label": "Package",
    "value": 18
  },
  {
    "id": "packs",
    "label": "Pack chính chủ",
    "value": 6
  },
  {
    "id": "scope",
    "label": "Hạng mục phạm vi PASS",
    "value": 10,
    "unit": "/ 18",
    "hint": "8 cái còn lại là PARTIAL"
  },
  {
    "id": "tests",
    "label": "Acceptance test PASS",
    "value": 77,
    "unit": "/ 77"
  }
]
```

## Theo chân một tin nhắn

Bạn gõ gì đó rồi Enter. Client web gửi lên /conversations/\{id\}/messages/stream, node trả lời bằng Server-Sent Events. Mọi route trừ /health, /openapi.json và tài liệu discovery đều cần bearer token của node, và thiếu token hay sai token đều nhận cùng một lỗi 401.

Việc đầu tiên node làm với câu của bạn là cố gắng không gọi model. Slash command kiểu /new là việc của host. App intent gõ bằng chữ, như bảo mở settings, cũng do host khớp và trả lời. Những thứ bạn trỏ tới bằng @, một project, một file, một cuộc hội thoại khác, được kiểm tra lại, và nếu thứ đó không còn nữa thì cả tin nhắn bị từ chối, nói rõ tên, thay vì mở một lượt trả lời mà thiếu đúng cái bạn hỏi.

Rồi tới một câu hỏi mà phần lớn app chat không hề hỏi: Clark có đang trả lời dở trong cuộc hội thoại này không? Nếu có, tin nhắn của bạn không chỉ đơn giản là xếp hàng. Một bước quyết định nhỏ, decideTurnAction, chọn giữa steer (nhập vào câu trả lời đang chạy), interrupt (thay thế nó) và background (chạy song song bên cạnh). Khi không đủ tự tin, nó rơi về interrupt, cái mà comment trong code gọi là hướng có thể sửa lại được, thay vì hướng im lặng.

Đường đi của một tin nhắn trong một node ClarkCant. Bạn gõ trong client web hoặc desktop, client gửi lên route messages/stream; gateway kiểm tra bearer token. Slash command, app intent và tham chiếu @ được host xử lý trước. Nếu Clark đang trả lời dở, một bước quyết định chọn steer, interrupt hoặc background. Sau đó tin nhắn tới conductor (handleUserMessage); câu nói qua voice socket cũng tới đúng conductor đó dưới dạng transcript cuối. Nếu có capability đã cài phù hợp, conductor tạo task và giao cho worker process; còn lại thì model trả lời trong một Pi session qua pi-adapter, được context planner chuẩn bị recap và memory brief. Model gọi node tools; tool có effect, và lệnh của worker, đi qua preflight, execution policy, guardrail và secret broker, rồi được ghi vào audit\_log và effect ledger. Tool show\_view đi tới view catalog, nơi dựng surface block đặt vào câu trả lời. Câu trả lời được ghi vào SQLite (messages và search index), được đọc to nếu lượt đó là giọng nói, và client vẽ surface block bằng widget renderer dùng chung.



Node và cạnh bám theo apps/runtime/src/routes/conversations.ts, packages/core/src/conductor.ts, apps/runtime/src/model-turn.ts và apps/runtime/src/voice-session.ts.

Tiếp theo tin nhắn tới conductor trong packages/core. Việc của nó hẹp, và đây là chỗ duy nhất đưa ra quyết định này: câu này thành một tin nhắn được trả lời, hay thành một task bền vững? Thứ tự là có chủ đích. Capability thật đã cài trước. Sau đó, chỉ khi đi đường demo, là một mẫu soạn sẵn có dán nhãn mẫu. Cuối cùng mới tới lời của model.

Trích đoạn handleUserMessage trong packages/core/src/conductor.ts, dòng 730 tới 758. Nó liệt kê capability dùng được và chọn execution node; chỉ khi không có node nào thì mới xét recipe mẫu, và recipe chỉ chạy khi tin nhắn đến từ đường demo; recipe bắt mọi câu bị bỏ khi đã có model. Sau đó, nếu không có execution node và có model, runModelTurn trả lời. Thứ tự: capability thật, rồi demo soạn sẵn, rồi lời của model.



packages/core/src/conductor.ts, dòng 730–758, trong handleUserMessage.

Nếu có capability làm được việc, conductor tạo task, đẩy nó qua state machine rồi giao cho một worker process. Trong state machine đó, thành công chỉ đạt được qua bước xác minh có bằng chứng ghi lại. Worker đứng im, model kết thúc lượt, hay socket đóng, đều không phải thành công. Còn nếu máy chưa có model nào và cũng chẳng có gì đã cài giúp được, task sẽ đứng chờ và Clark nói thẳng ra như vậy, không tự chế.

Nhưng phần lớn thời gian thì model trả lời. Lượt trả lời chạy ngay trong process của node, trên một Pi session. Pi là agent loop, và ClarkCant không fork nó. packages/pi-adapter là package duy nhất import Pi SDK; mọi thứ khác chỉ phụ thuộc vào interface PiAdapter. Có RealPiAdapter typed theo SDK và FakePiAdapter chạy tất định, nhờ vậy cả app chạy được trên CI mà không cần tài khoản provider nào.

Trước khi prompt được gửi đi, context planner quyết định model được biết những gì. Một session mới cho cuộc hội thoại đã có sẵn sẽ nhận recap: 12 tin mới nhất trong 40 tin gần nhất, cộng tối đa 4 tin cũ hơn có chung từ khóa với câu bạn vừa hỏi. Mỗi lượt còn có memory brief dựng từ những record bạn xem được. Mọi block host thêm vào đều được gắn nhãn public, internal, confidential hoặc secret chỉ dựa trên nội dung chữ, và một model profile có thể bị giới hạn chỉ được nhận một số mức nhất định.

## Widget là bộ từ vựng, không phải tấm canvas

Đây là phần tui tâm đắc nhất. Câu trả lời có thể chứa nhiều thứ hơn chữ, nhưng model không bao giờ tự viết UI. Nó gọi một tool tên show\_view, đưa tên một view trong catalog do host cung cấp cùng vài giá trị. View catalog của host (apps/runtime/src/view-catalog.ts) kiểm tra mấy giá trị đó rồi tự dựng block: một surface block trỏ tới định nghĩa widget, kèm một snapshot có phần mô tả bằng chữ.

Comment đầu file đó nói đúng điểm mấu chốt: catalog là thứ khiến một approval giả mạo không thể biểu diễn được, chứ không chỉ là bị từ chối. Trong schema của tool không có tham số nào tên type, owner hay status. Model đơn giản là không có từ nào để mô tả một card thuộc về host. Card phê duyệt, card nhập credential và trạng thái task đều do node dựng từ record của chính nó, mà model thì không có record nào để dựng.

Widget chia theo làn tin cậy. Widget built-in trong catalog là code React đáng tin, chỉ vẽ props đã qua schema. Composition dạng khai báo thì sắp xếp component có sẵn, không mang theo code thực thi. UI của bên thứ ba chạy trong iframe cô lập với opaque origin, không bao giờ chung origin với khung chat, và nói chuyện với host qua một bridge. Action mà widget đưa ra được biên dịch thành binding, và host xác thực lại mỗi lần bấm; widget không gọi được tool chỉ vì biết tên nó.

Catalog nằm ở packages/widget-catalog, renderer nằm ở packages/conversation-client. Chi tiết thứ hai quan trọng hơn vẻ ngoài. Đúng bộ renderer đó được export qua một entry công khai, public-widget.tsx, gắn một widget vào shadow root chỉ với view state cục bộ: không action, không kết nối runtime, không phê duyệt, không credential. Nếu bạn đang đọc bài này với JavaScript bật, sơ đồ phía trên chính là do entry đó vẽ. Blog này bundle nó thẳng từ source ClarkCant chứ không copy.

## Tự chủ, và ai giữ phanh

ClarkCant mặc định là tự chủ. Trong code, các execution mode là autonomous, guarded và ask, mặc định là autonomous. Nghe thì liều, cho tới khi bạn thấy quyền thật sự nằm ở đâu.

Mọi effect, dù là một lệnh, một lần ghi file hay một action của widget làm nhiều hơn đổi cách hiển thị, đều đi qua cùng một thứ tự. Đầu tiên là host preflight (apps/runtime/src/preflight.ts): tất định, không gọi model, không cãi được. Nó kiểm tra effect có nằm trong thư mục node này sở hữu không, mục tiêu và capability có tồn tại không, rồi gắn deadline và giới hạn output. Sau đó execution policy trong packages/core quyết định ai sẽ được hỏi, theo thứ tự cố định: lệnh cấm bạn đặt thắng ranh giới đồng ý của OS hay OAuth, ranh giới đó thắng luật bạn viết, luật thắng mode. Rồi tới guardrail của Jev, có thể allow, deny, constrain hoặc hỏi lại cho rõ, và chỉ được siết lại chứ không bao giờ nới ra. Cuối cùng secret broker bơm credential vào đúng lúc cần.

Cái cuối là tính năng nhàm chán mà tui thích nhất. Agent biết là có github\_token, dùng vào việc gì, ai được dùng. Còn giá trị thì đi từ backend vào đúng một lần gọi, biến môi trường của một process con hoặc một header, và không bao giờ quay lại model. Nếu lệnh nhận secret mà lỡ in nó ra, mọi giá trị đã bơm vào dài từ tám ký tự trở lên đều bị thay bằng \[redacted\] trước khi output tới cuộc hội thoại.

Mọi thứ có effect đều vào audit\_log, một bảng chỉ ghi thêm, và cố ý làm nông: cái gì chạy, lúc nào, kết thúc ra sao. Không bao giờ có tham số, không bao giờ có giá trị. Và có một nút đỏ to: POST /stop giết process group của mọi lệnh, worker và terminal đang chạy, ngắt các lượt trả lời và hủy luôn việc nền đang xếp hàng.

Ai quyết, theo thứ tự

Lệnh cấm bạn đặt thắng ranh giới consent của OS hay OAuth. Ranh giới đó thắng luật bạn viết. Luật của bạn thắng execution mode. Guardrail chỉ được siết phần còn lại.

## Memory đọc được và xóa được

Memory không phải một cục vector bí ẩn. Khi Clark thấy điều gì đáng nhớ, tool remember ghi một câu vào memory\_records, đã redact trước khi ghi, kèm cuộc hội thoại nơi câu đó được học. Brief đưa vào mỗi lượt được đọc mới từ database ở từng lượt, không cache trong process. Xóa một memory trong tab Memory là lượt sau nó hết vào prompt, không phải đợi restart. Tin nhắn gốc của bạn vẫn nằm trong cuộc hội thoại: xóa memory là xóa đường nó quay lại prompt, không phải xóa lịch sử của bạn.

Tìm kiếm lịch sử mặc định là lexical, SQLite FTS5 với BM25. Tụi tui có build cả đường semantic, sqlite-vec với model E5-small đã lượng tử hóa và reciprocal rank fusion, rồi đo. Trên bộ dữ liệu có gán nhãn, FTS thuần trả đúng kết quả đầu ở 31 trên 34 câu hỏi. Hybrid cũng 31 ở cấu hình tốt nhất và tụt còn 25 ở cấu hình lỏng hơn, vì tìm láng giềng gần nhất thì lúc nào cũng trả về một cái gì đó, kể cả khi câu trả lời thật là không có gì. Nên semantic search tắt mặc định, code nằm chờ sau một flag cho tới khi có bộ dữ liệu lớn hơn chứng minh điều ngược lại. Tui thích những quyết định có con số đi kèm.

## Voice là giọng nói, không phải bộ não

Voice chạy trên Gemini Live, model mặc định gemini-3.8-live, đứng sau một WebSocket proxy trên node. Trình duyệt không bao giờ giữ credential của provider, kể cả token ngắn hạn. Nó gửi audio PCM16 16 kHz lên node và nhận audio về. Frame đầu tiên của socket bắt buộc là frame xác thực, vì token nằm trong URL WebSocket thì kiểu gì cũng lọt vào access log.

Phần mất khá lâu mới làm đúng: model live không được phép tự trả lời. Câu bạn nói được chuyển thành chữ, và transcript cuối đi qua handleUserMessage, đúng cái conductor mà tin nhắn gõ tay đi qua. Agent trả lời bằng tool, memory và ngữ cảnh của nó, còn việc duy nhất của phiên live là đọc to câu trả lời đó. Không vậy thì trong phòng sẽ có hai câu trả lời, mà chỉ một trong hai là đã đọc file của bạn.

```typescript
/**
 * What the live session is for.
 *
 * It is the voice, not the mind. Left to itself the model answers whatever it hears, and then the
 * same question has two answers in the room: the model's guess, made without any tool and without
 * the conversation, and the agent's, which is the only one of the two that read the files, ran the
 * command or knows what was said five minutes ago. So the session is told to transcribe and to read
 * back, and to leave answering to the agent.
 */
const VOICE_INSTRUCTION = [
  "Bạn là giọng nói của trợ lý, không phải bộ não của nó.",
  "Có hai loại đầu vào và hai việc khác nhau, đừng lẫn chúng với nhau.",
  /*
   * Silence while the person speaks.
   *
   * This used to ask the model to transcribe what it heard, and the transcription was already being made
   * without it: both transcription configs are on, and the provider's own reading of the input is what the
   * transcript is built from. So the only thing that instruction added was the model saying those words out
   * loud - the person's own sentence, read back to them by their assistant before it had any answer to give.
   */
  "Khi nghe tiếng người dùng nói: giữ im lặng, không nói gì, không chép lại, không trả lời, không hỏi lại, không bình luận.",
  "Khi nhận được một lượt văn bản: đó là câu trả lời của trợ lý, và việc của bạn là đọc nguyên văn đoạn văn đó ngay lập tức, không thêm bớt chữ nào.",
].join(" ");

/**
```

apps/runtime/src/voice-session.ts, dòng 445–469. Bốn câu chỉ dẫn được viết bằng tiếng Việt, đúng như trong source.

Voice cũng biết bạn đang nhìn vào đâu. Client báo cho node widget nào đang được focus, nên câu “dời cái đó sang thứ Sáu” có mục tiêu rõ ràng, và trả lời một question card bằng giọng nói đi qua đúng cái hàm mà một cú click dùng. Voice không phải một sản phẩm thứ hai với đường code riêng.

## Bộ máy mà bạn không cần thấy

ClarkCant không có ô chọn session, cố ý vậy. Bên dưới, một cuộc hội thoại vẫn có một Pi session, và có khi qua nhiều session trong đời nó. Đổi model bằng Cmd/Ctrl+\] thì session đang chạy không bị đụng tới: node ghi lại lựa chọn của bạn, và tới ranh giới lượt kế tiếp thì chuyển sang một thế hệ mới, được brief bằng recap. Session bị dọn đi sau khi ngồi không lâu cũng được dựng lại đúng kiểu đó. Từ phía bạn, nó vẫn là một cuộc hội thoại.

Việc nền cũng vậy. Background request, process của run\_command, task worker và terminal đều đăng ký với một WorkSupervisor duy nhất. Mặc định tối đa 3 việc nền chạy cùng lúc (Settings cho chọn 1, 3 hoặc 5), hàng đợi tối đa 10. Mỗi process con có process group riêng, nên dừng một lệnh là dừng luôn cái sleep 60 & nó để lại. Sau khi crash, mỗi việc dang dở được báo đúng một lần trong cuộc hội thoại đã yêu cầu nó, và lệnh có effect không bao giờ tự chạy lại. Bạn có thể hỏi Clark cái gì đang chạy hoặc mở process panel, nhưng không bao giờ phải tự quản.

Các node đã pair cũng theo luật đó. Hiện tại home node có thể giao một task có phạm vi cho peer đã xác nhận, thông qua một automation chỉ định peer đó làm executor. Grant giao nhau chứ không cộng dồn, nên A pair với B và B pair với C thì A chẳng có quyền gì trên C, và policy của chính node nhận việc vẫn áp lên mọi effect. Bạn không cần biết topology mới hỏi được trạng thái, nhưng khi một hành động thật sự quan trọng thì nó luôn cho thấy đang nhắm vào đâu.

## Mọi thứ nằm ở đâu

Cây thư mục của monorepo ClarkCant. apps/runtime: Node headless: gateway HTTP và WebSocket, danh tính, vòng đời, health; apps/web: Client trình duyệt, dùng chung component hội thoại với desktop; apps/desktop: Vỏ Electron mỏng: chỉ IPC có kiểu, không chạy scheduler; apps/worker: Chạy một Pi session do app quản lý cho mỗi run, báo cáo bằng chứng; apps/cli: Lệnh clarkcant: hỏi Clark, đọc hội thoại, MCP qua stdio; packages/core: Bất biến: state machine task/run, policy, grant, lease, capability registry; packages/contracts: Hợp đồng được validate lúc chạy: command, event, task, effect, widget, NodeLink; packages/storage: SQLite WAL, migration, outbox/inbox, effect ledger, backup; packages/pi-adapter: Package duy nhất import Pi SDK; packages/conversation-client: UI React dùng chung: timeline, composer, card, widget renderer; packages/widget-catalog: Catalog widget chuẩn, fixture, kiểm tra props; packages/widget-host: Registry built-in, sandbox policy cho mini-app, biên dịch action; packages/widget-sdk: Hợp đồng cho người viết widget: props, state, event, action, semantic; packages/widget-cli: Tạo khung, chạy conformance test và đóng gói widget; packages/design-tokens: Design token, appearance compiler, kiểm tra tương phản WCAG; packages/node-link: Giao thức giữa các node: envelope, thương lượng version, dedup, grant; packages/capability-host: Khám phá capability, kích hoạt theo giai đoạn, rollback; packages/execution-supervisor: Profile chạy process với env tối thiểu, lease có epoch fencing; packages/host-adapters: Backend cho credential vault, nhập liệu an toàn, dò khả năng của OS; packages/integration-sdk: Helper OAuth PKCE, giữ API key, kiểm tra scope, readiness probe; packages/mcp-adapters: Adapter tool/auth cho MCP và cầu nối MCP Apps; packages/voice-adapters: Mô hình event giọng nói trung lập, adapter Gemini Live và TTS; packages/signal-sources: Xác minh và chuẩn hóa tín hiệu đến, GitHub trước tiên; packs/browser-playwright: Browser Use: contract cho target, quan sát và action; cần tải browser engine; packs/computer-linux-desktop: Desktop ảo Linux: contract cho session và lease; cần display server; packs/computer-macos: Driver native macOS: lease bàn phím chuột và consent; cần quyền Accessibility; packs/data-canvas: Mô tả widget chart, table, map, timeline trên dataset ref; packs/google-calendar: Tích hợp mẫu; truy cập thật cần OAuth client đã đăng ký; packs/project-work: Hỏi đáp file chỉ đọc và một code task có kiểm soát.



Trách nhiệm tóm từ mô tả trong package.json của từng app, package và pack.

Năm app, mười tám package, sáu pack chính chủ. Ranh giới tui cứ phải bảo vệ hoài là ranh giới giữa core và phần còn lại. packages/core giữ các bất biến: state machine của task, đối soát effect, giao nhau giữa policy và grant, lease, capability registry. Tính năng theo miền nằm trong pack (trình duyệt, lịch, làm việc với project, driver desktop) và chạm vào core qua contract, không bao giờ đi vòng. Pack được đóng góp tool, widget và công thức cài đặt. Pack không được mang theo scheduler riêng, kho hội thoại riêng hay một tài khoản root giấu kín.

## Cái gì chưa chạy

ClarkCant là bản bootstrap, chưa phải bản release, và README nói điều đó trước mọi thứ khác. Nó có hẳn một mục tên là “What does not work yet”. OAuth chạy thật, tích hợp Google Calendar, driver native cho macOS và Linux, tải package thật từ git hay npm, và một luồng consent thật để cấp capability cho package vừa cài, đều nằm trong danh sách đó. Contract, state machine và các lời từ chối của chúng đã được implement và có test. Còn transport và luồng chạy trọn vẹn từ đầu tới cuối thì chưa.

docs/conformance-traceability.md là cuốn sổ tui tin hơn mọi roadmap. Mỗi hạng mục trong 18 hạng mục phạm vi có trạng thái gắn với một test có tên, và pnpm invariants làm fail build nếu cuốn sổ lệch với registry dạng máy đọc. Hiện tại 10 hạng mục PASS, 8 hạng mục PARTIAL, và dòng PARTIAL nào cũng ghi rõ còn thiếu lớp nào. Cả 77 acceptance test đều PASS, kèm lời cảnh báo ngay trong tài liệu: có test không có nghĩa là lần chạy hiện tại đã qua.

Bảng trạng thái theo docs/conformance-traceability.md. PASS (10): V01 Client hội thoại, V02 Runtime di động, V03 Runtime task/session bền vững, V04 Liên kết node tin cậy, V05 Cộng tác từ xa, V06 Registry workspace, V07 Nền tảng capability, V11 Widget built-in, V16 Onboarding/cá nhân hóa, V18 Vận hành/bảo mật. PARTIAL (8): V08 Cài đặt bằng hội thoại, V09 Thiết lập credential/auth, V10 Tích hợp mẫu (Google Calendar), V12 Widget tùy biến, V13 Pin, V14 Browser Use, V15 Computer Use, V17 Voice trực tiếp.



Cột trạng thái của docs/conformance-traceability.md, ngày 5/10/2026.

Tui thà bạn đọc cái bảng đó còn hơn tự phát hiện ra. Commit đầu tiên của repo ghi ngày 16 tháng 9 năm 2026, còn hôm tui viết bài này là 5 tháng 10. Nó đi nhanh, và tui muốn nó đi nhanh một cách công khai.

Nếu chỉ nhớ một ý: trong ClarkCant, model đề xuất, host quyết định. Model chọn chữ, chọn tool, chọn view. Host giữ danh tính, quyền, secret, trạng thái task, định nghĩa thế nào là thành công, và cái gì được vẽ thành card đáng tin. Cả bài này chỉ là câu đó được áp dụng đi áp dụng lại.

Có thể tui sai ở chỗ đặt vài ranh giới. Code là Apache-2.0, ở github.com/digitopvn/clarkcant. Đọc đi, phá đi, rồi kể tui nghe.
