Pify

Chương 10: Quản lý session: lưu, tiếp tục và phân nhánh hội thoại

Cách Pi lưu một session thành cây JSONL nối bằng parent, dựng lại context đang hoạt động và cung cấp SessionManager.

Chương 9 kết thúc với một CompactionEntry trên Session Tree. Mô tả ngắn đó còn để lại hai câu hỏi: cây nằm ở đâu, và buildSessionContext() biến cây trở lại danh sách message tuyến tính mà mỗi lần gọi model cần bằng cách nào?

Coding Agent của Pi giải quyết cả hai bằng một file session bền vững. Mỗi bản ghi sau header trỏ đến nút cha, một nút lá trong bộ nhớ đánh dấu vị trí hiện tại, còn bước dựng context chiếu ra đúng một đường từ gốc đến nút lá. Vì vậy, cùng một file có thể giữ nhiều hướng xử lý trong khi model chỉ thấy nhánh mà người dùng đã chọn.

1. Bắt đầu từ hai câu hỏi về lưu trữ

context.messages là một mảng tại ranh giới model. Mảng này không cho biết hội thoại tồn tại ở đâu sau khi tiến trình kết thúc, cũng không cho biết ứng dụng giữ lịch sử theo hình dạng nào. Thiết kế lưu bền vững nên trả lời riêng hai câu hỏi:

  • Nơi lưu trữ: bản ghi được ghi ở đâu?
  • Cấu trúc logic: các bản ghi liên hệ với nhau ra sao?

Cơ sở dữ liệu có thể lưu một cây, còn file có thể lưu một chuỗi tuyến tính. Chọn nơi lưu không tự quyết định cấu trúc.

Câu hỏi A: session được lưu ở đâu?

Mặc định, Coding Agent ghi JSONL cục bộ. Session nằm dưới ~/.pi/agent/sessions/; thư mục làm việc được mã hóa thành một thư mục như --work-project--, và mỗi session có đúng một file .jsonl. File không nằm trong .pi/sessions/ của dự án. Việc nhóm theo dự án diễn ra bên dưới thư mục Agent của người dùng.

Lựa chọn này hợp với một CLI chạy cục bộ. Nó không cần dịch vụ cơ sở dữ liệu, bản ghi có thể đọc bằng công cụ văn bản thông thường, và khi chuyển dự án, Pi tự chọn thư mục đã mã hóa tương ứng. JSONL không cung cấp giao dịch kiểu cơ sở dữ liệu, truy vấn xuyên máy hay điều phối nhiều tiến trình ghi. Nếu sản phẩm cần các đặc tính đó, chúng phải được thiết kế riêng.

Các lệnh có sẵn xử lý việc ghép đường dẫn thay cho mã gọi:

pi -c                  # Tiếp tục session gần nhất
pi -r                  # Duyệt các session
pi --no-session        # Chỉ giữ run trong bộ nhớ
pi --name "auth audit" # Đặt tên session mới
pi --session <path|id> # Mở một session
pi --fork <path|id>    # Chép lịch sử sang session mới

/session hiển thị file, ID, số message, tổng token và chi phí hiện tại. /resume, /new, /name, /tree, /fork và /clone hoạt động ở tầng sản phẩm cao hơn.

Agent Core quản lý transcript trong bộ nhớ và Agent event; Pi v1.0.0 không export bộ khung lưu session từ gói này. SessionManager đồng bộ của Coding Agent quản lý cây JSONL này, còn AgentSession điều phối runtime đang chạy. Pi Durable cung cấp nền tảng conversation và Documents thử nghiệm riêng. Phần 7 phân định các ranh giới sở hữu này.

Câu hỏi B: session có cấu trúc gì?

Bản ghi hội thoại tuyến tính đủ dùng cho đến khi người dùng muốn thử lại từ một điểm cũ, so sánh hai hướng hoặc quay về branch đã bỏ. Nếu chỉ viết lại một mảng, ứng dụng phải bỏ đoạn đuôi cũ hoặc sao chép đoạn đó sang nơi khác.

Coding Agent giữ một cây nối bằng parent. Append thêm bản ghi; rewind di chuyển leaf hiện tại; lần append tiếp theo tạo nút con mới bên dưới entry được chọn. Các nút con cũ vẫn ở trong bộ nhớ và file JSONL. Chỉ khi người dùng chủ động fork hoặc clone thì Pi mới tạo file khác.

Chiều thiết kếLựa chọn phổ biếnMặc định của Coding Agent
Nơi lưu trữCơ sở dữ liệu quan hệ hoặc cơ sở dữ liệu dạng tài liệuMột file JSONL cục bộ cho mỗi session
Cấu trúc logicBản ghi hội thoại tuyến tínhCây chỉ ghi thêm, nối bằng parent

Hai lựa chọn này dẫn đến toàn bộ phần còn lại của chương. JSONL tạo thứ tự vật lý; id, parentId và leafId tạo cây logic.

2. Xây Session Tree theo từng bước

Xét một lỗi xác thực. Nhà phát triển đổi model, hỏi vì sao bước kiểm tra salt thất bại, để Agent xem hai hàm, bác chẩn đoán đầu tiên rồi thử một hướng khác.

Đây là dàn ý tình huống, không phải JSONL nguyên văn:

1. Chuyển sang anthropic/claude-sonnet-4-6.
2. Hỏi vì sao salt verification thất bại trong src/auth.ts.
3. Assistant gọi read.
4. read trả về nội dung file.
5. Assistant cho rằng phép so sánh ở dòng 23 có lỗi.
6. Chuyển leaf về câu hỏi.
7. Yêu cầu xem bản triển khai của hàm hash trước.
8. Assistant gọi grep và read, rồi đưa ra chẩn đoán mới.

Các sơ đồ dùng bí danh ngắn e1, e2, v.v. Entry ID thực mà Coding Agent lưu là tiền tố UUID tám ký tự đã kiểm tra trùng, không phải các bí danh minh họa này.

Bước 1: đổi model và tạo root entry

Dòng vật lý đầu tiên là SessionHeader, tức siêu dữ liệu chứ không phải nút trong cây. Khi đổi model, Pi append ModelChangeEntry đầu tiên. Dữ liệu thực có cả provider và modelId:

e1: ModelChangeEntry
  parentId: null
  provider: "anthropic"
  modelId: "claude-sonnet-4-6"

Leaf trong bộ nhớ tiến đến e1:

e1 model_change  <- leafId

parentId: null đánh dấu một root entry. Header không có id của entry nên không thể làm parent.

Bước 2: append user message

Câu hỏi trở thành SessionMessageEntry; trường message của nó là một UserMessage thuộc Pi AI. Bản phác thảo này lược bỏ timestamp ISO của entry và timestamp Unix mili giây của message. Ví dụ lưu trữ ở Phần 6 sẽ có cả hai.

e2: SessionMessageEntry
  parentId: e1
  message:
    role: "user"
    content: "Vì sao salt verification thất bại trong src/auth.ts?"

Liên kết parent kéo dài đường dẫn đang hoạt động:

e1 model_change
└─ e2 user  <- leafId

e2 trỏ đến entry đứng trước nó trên branch này, chứ không nhất thiết là dòng vật lý đứng trước. Khi cây còn tuyến tính, hai thứ tự tình cờ trùng nhau.

Bước 3: lưu lời gọi Tool của assistant

Assistant quyết định xem src/auth.ts. Text và lệnh gọi read cùng nằm trong một mảng AssistantMessage.content:

e3: SessionMessageEntry
  parentId: e2
  message:
    role: "assistant"
    content:
      - { type: "text", text: "Tôi sẽ xem hàm verification." }
      - { type: "toolCall", id: "call_001", name: "read",
          arguments: { path: "src/auth.ts" } }
    stopReason: "toolUse"

Cây vẫn chỉ có một branch:

e1 model_change
└─ e2 user
   └─ e3 assistant + lệnh gọi read  <- leafId

Message của assistant được lưu còn có đầy đủ api, provider, model, số liệu usage và timestamp dạng số. Các trường đó mô tả phản hồi hoàn chỉnh của provider; session entry bao ngoài có timestamp ISO riêng.

Bước 4: nối kết quả Tool với lời gọi tương ứng

Sau khi Tool chạy xong, Pi tạo một ToolResultMessage riêng trong SessionMessageEntry kế tiếp:

e4: SessionMessageEntry
  parentId: e3
  message:
    role: "toolResult"
    toolCallId: "call_001"
    toolName: "read"
    content: [{ type: "text", text: "export function verifySalt(...) { ... }" }]
    isError: false

Kết quả nối tiếp cùng chuỗi parent:

e1 model_change
└─ e2 user
   └─ e3 assistant + lệnh gọi read
      └─ e4 kết quả read  <- leafId

toolCallId khớp với ToolCall.id trong e3; toolName cho biết Tool nào đã chạy. Liên kết parent sắp thứ tự session entry, còn ID của lời gọi Tool ghi lại quan hệ giao thức giữa hai message.

Bước 5: append chẩn đoán đầu tiên

Assistant đọc kết quả Tool rồi trả lời:

e5: SessionMessageEntry
  parentId: e4
  message:
    role: "assistant"
    content: [{ type: "text", text: "Phép so sánh ở dòng 23 dùng sai cách mã hóa." }]
    stopReason: "stop"

Lần thử đầu tiên trở thành một đường dẫn gồm năm entry:

e1 model_change
└─ e2 user: salt verification
   └─ e3 assistant: read
      └─ e4 toolResult: auth.ts
         └─ e5 assistant: chẩn đoán dòng 23  <- leafId

Mỗi lần append tạo một entry mới rồi đẩy leafId tới entry đó. Không entry cũ nào được thêm danh sách nút con hay bị sửa nội dung.

Bước 6: rewind bằng cách di chuyển leaf

Người lập trình muốn thử cách khác. Thao tác điều hướng bằng API có thể đặt leaf tại e2. Đây là mã giả bám theo mã nguồn; phương thức công khai branch() kiểm tra entry tồn tại rồi gán con trỏ theo cách đồng bộ:

if (!byId.has("e2")) throw new Error("Entry e2 not found");
leafId = "e2";

Đoạn đuôi cũ vẫn còn nguyên:

e1 model_change
└─ e2 user: salt verification  <- leafId
   └─ e3 assistant: read
      └─ e4 toolResult: auth.ts
         └─ e5 assistant: chẩn đoán dòng 23

Việc di chuyển con trỏ chỉ diễn ra trong bộ nhớ. leafId không phải trường trong header v3 của Coding Agent, còn branch() không append bản ghi điều hướng. Lần append sau đó mới ghi branch bền vững thông qua parentId. Nếu tiến trình thoát ngay sau branch(), khi mở lại, Pi lấy entry vật lý cuối cùng làm leaf.

Bước 7: append bên dưới vị trí đã rewind

Người lập trình đặt câu hỏi hẹp hơn. Vì leaf hiện tại là e2, user entry mới trỏ đến e2, giống e3:

e6: SessionMessageEntry
  parentId: e2
  message:
    role: "user"
    content: "Hãy xem bản triển khai của hàm hash trước verifier."

Hai nút con có cùng parent tạo thành hai branch:

e1 model_change
└─ e2 user: salt verification
   ├─ e3 assistant: hướng đầu tiên
   │  └─ e4 toolResult
   │     └─ e5 assistant: chẩn đoán đầu tiên
   └─ e6 user: xem hash trước  <- leafId

Ví dụ bằng API này chủ động append message thứ hai của người dùng sau e2. Trong chế độ tương tác, /tree xử lý khác khi entry được chọn là message của người dùng hoặc message tùy chỉnh: nó chuyển tới parent của entry, chép văn bản đã chọn vào trình soạn thảo và để lần gửi đã sửa trở thành nút cùng cha với entry được chọn.

Bước 8: tiếp tục branch mới

Assistant yêu cầu cả tìm kiếm lẫn đọc file. Mỗi ToolResultMessage trả về trở thành một session entry riêng trước câu trả lời cuối:

e1 model_change
└─ e2 user: salt verification
   ├─ e3 assistant: hướng đầu tiên
   │  └─ e4 kết quả read
   │     └─ e5 assistant: chẩn đoán đầu tiên
   └─ e6 user: xem hash trước
      └─ e7 assistant: lệnh gọi grep + read
         └─ e8 kết quả grep
            └─ e9 kết quả read
               └─ e10 assistant: chẩn đoán mới  <- leafId

File giờ có mười entry của cây theo thứ tự append. Đường dẫn đang hoạt động là e1 -> e2 -> e6 -> e7 -> e8 -> e9 -> e10. Các entry e3 đến e5 vẫn có thể truy cập bằng getEntry(), vẫn xuất hiện trong getTree() và vẫn có thể được chọn lại, nhưng không đi vào context của lần gọi model kế tiếp.

3. Đọc một session entry

Cây trở nên cụ thể khi ta xem một bản ghi đã lưu.

Một SessionMessageEntry đầy đủ

Đây là đối tượng JSON hợp lệ cho lời gọi Tool của assistant. Nó có mọi trường bắt buộc trong AssistantMessage hiện tại của Pi AI; token và chi phí thực tế đến từ provider.

{
  "type": "message",
  "id": "c3d4e5f6",
  "parentId": "b2c3d4e5",
  "timestamp": "2026-08-24T10:23:30.000Z",
  "message": {
    "role": "assistant",
    "content": [
      { "type": "text", "text": "Tôi sẽ xem hàm verification." },
      {
        "type": "toolCall",
        "id": "call_001",
        "name": "read",
        "arguments": { "path": "src/auth.ts" }
      }
    ],
    "api": "anthropic-messages",
    "provider": "anthropic",
    "model": "claude-sonnet-4-6",
    "usage": {
      "input": 1250,
      "output": 80,
      "cacheRead": 0,
      "cacheWrite": 0,
      "totalTokens": 1330,
      "cost": {
        "input": 0,
        "output": 0,
        "cacheRead": 0,
        "cacheWrite": 0,
        "total": 0
      }
    },
    "stopReason": "toolUse",
    "timestamp": 1787567010000
  }
}
TrườngÝ nghĩa khi chạyLý do lưu
typePhân biệt thành viên trong kiểu hợp entryCây có message, thay đổi trạng thái, bản tóm tắt và siêu dữ liệu
idĐịnh danh entry nàyNút con và label tham chiếu entry bằng ID
parentIdTrỏ đến entry đứng trước trên branchMột con trỏ đủ để dựng đường dẫn từ root đến leaf
timestamp của entryThời điểm tạo dạng ISODùng khi sắp xếp session, hiển thị cây và gỡ lỗi
messageDữ liệu AgentMessage đầy đủKhi tiếp tục, Pi giữ phản hồi của provider, số liệu sử dụng và trường giao thức Tool
timestamp của messageUnix mili giâyMessage thuộc Pi AI giữ cách biểu diễn thời gian riêng

Khi SessionManager đọc file v3, nó không kiểm tra đầy đủ schema của từng dòng đã phân tích cú pháp. Hãy xem định nghĩa TypeScript là quy ước định dạng; việc một đối tượng JSON phân tích cú pháp được chưa đủ chứng minh nó an toàn.

Mười một loại entry của Coding Agent

SessionEntry là union gồm mười một kiểu. Chia theo tác động lên context giúp ta thấy lý do chúng tồn tại riêng.

Bốn kiểu entry có thể chiếu một hoặc nhiều message vào context đang hoạt động:

Kiểu entryKết quả chiếu
SessionMessageEntry (message)Trả về AgentMessage đã lưu; appendMessage() nhận message của người dùng, assistant, kết quả Tool, message tùy chỉnh và lệnh Bash, còn bản tóm tắt dùng entry riêng
CustomMessageEntry (custom_message)Tạo CustomMessage với customType, content, display, details và timestamp của entry
CompactionEntry (compaction)Tạo CompactionSummaryMessage; buildContextEntries() còn áp dụng ranh giới giữ lại
BranchSummaryEntry (branch_summary)Tạo BranchSummaryMessage gắn với fromId

Hai kiểu entry thay đổi trạng thái trả về bên cạnh messages:

Kiểu entryTác động lên trạng thái
ModelChangeEntry (model_change)Đặt { provider, modelId } cho đường dẫn đã chọn
ThinkingLevelChangeEntry (thinking_level_change)Đặt chuỗi thinkingLevel hiện tại

ContextEditEntry (context_edit) điều khiển projection mà không tự tạo message. targetId trỏ tới một entry trước đó trên path đã chọn. replacement: null loại target khỏi provider context về sau; giá trị { content } khác null chỉ thay content, còn role và message metadata được giữ nguyên.

Bốn kiểu entry lưu metadata nhưng không đi vào context của model:

Kiểu entryMục đích
UsageEntry (usage)Lưu usage gắn với model nhưng không đóng góp message
CustomEntry (custom)Lưu data của Extension dưới một customType
LabelEntry (label)Đặt hoặc xóa label trên targetId; thay đổi được append sau cùng có hiệu lực
SessionInfoEntry (session_info)Đặt hoặc xóa name hiển thị; entry được append sau cùng có hiệu lực

SessionHeader ở đầu file là một file-record shape khác, nhưng nó không phải SessionEntry và không có parent. Pi Durable định nghĩa conversation entry và Documents riêng; chúng không dùng schema JSONL này của Coding Agent. Trộn các schema sẽ tạo parser sai.

Context edit cũng là append-only tree entry:

Thao tácThay đổi được lưuẢnh hưởng lên projection
appendContextEdit(targetEntryId, null)thêm entry context_editloại entry đích khỏi provider context về sau
appendContextEdit(targetEntryId, { content })thêm entry context_editthay content của entry đích trong provider context về sau
transcript thô / lịch sử UIchỉ ghi thêm (append-only)không đổi
editId trả vềentry context_edit mớikhông phải entry đích
appendContextEdit(targetEntryId, null) -> context_edit(editId) -> provider context - targetEntryId
appendContextEdit(targetEntryId, { content }) -> context_edit(editId) -> provider context[targetEntryId] = content
context_edit -> raw transcript append-only
editId != targetEntryId

ID trả về thuộc edit entry vừa được tạo, không thuộc target. Switch exhaustive trên SessionEntry phải xử lý context_edit, dù sessionEntryToContextMessages() không tạo message cho chính edit entry.

Vì sao entry chỉ lưu parent

Con trỏ parent cho phép append mà không chạm vào entry cũ. Nếu e2 lưu children: [e3], khi thêm e6, hệ thống phải viết lại nó thành children: [e3, e6]. Với parentId, chỉ entry mới ghi quan hệ đó.

Coding Agent giữ byId: Map<string, SessionEntry> để tra cứu parent trực tiếp và duyệt từ leaf về root. getChildren(parentId) tìm nút con bằng cách quét các giá trị trong map; không có chỉ mục nút con thứ hai được lưu. getTree() cũng quét, coi entry mất parent là root, gắn label đã xác định và sắp xếp mỗi mảng nút con theo timestamp ISO.

Cách biểu diễn này giúp append trong bộ nhớ và branch() có chi phí trung bình O(1). Dựng toàn bộ dạng xem nút con cần một lượt quét, còn I/O có chi phí riêng. Con trỏ parent tránh phải viết lại entry cũ; nó không biến mọi truy vấn cây thành O(1).

4. Append, rewind, branch và tóm tắt

Từ ví dụ trên, ta có thể rút ra bốn thao tác mà vẫn giữ đúng hành vi sản phẩm.

Thao tác 1: append một nút con

Mã giả bám theo mã nguồn dưới đây mô tả _appendEntry() cùng các mã gọi. Đây là mã giả vì bản triển khai chia bước tạo entry cho nhiều phương thức appendXXX() có kiểu cụ thể.

id = generateCollisionCheckedId(byId)
entry = { type, id, parentId: leafId, timestamp: nowIso(), ...payload }
fileEntries.push(entry)
byId.set(id, entry)
leafId = id
persistAccordingToLazyWritePolicy(entry)
return id

Với message thông thường, mã gọi dùng API công khai thay vì tự tạo entry:

entryId = session.appendMessage(agentMessage)

Các thay đổi trong bộ nhớ gồm thao tác append, Map.set và gán con trỏ. SessionManager ghi file đồng bộ, nên thời gian thực tế còn gồm tạo file, ghi toàn bộ file lần đầu hoặc appendFileSync, tùy trạng thái.

Thao tác 2: rewind con trỏ hiện tại

branch(entryId) kiểm tra entry tồn tại rồi gán leafId. resetLeaf() đặt leaf thành null, nhờ đó lần append kế tiếp có thể tạo root khác.

branch("e2")
// leafId giờ là "e2"; e3, e4 và e5 không thay đổi.

Lời gọi này không tính hay lưu một đối tượng branch. Đường dẫn được suy ra sau bằng cách đi theo parent. Lần append kế tiếp mới cung cấp bằng chứng bền vững cho hướng mới.

Thao tác 3: append sau khi rewind

Sau khi leaf chuyển vị trí, append thông thường tạo branch. Không có kiểu branch riêng:

branch("e2")
e6 = appendMessage(revisedQuestion) // e6.parentId === "e2"

Các thao tác tương tác phân biệt việc giữ branch trong cùng file với việc chép một đường dẫn sang file khác:

Thao tácKết quả về fileHành vi chọn
/treeCùng file sessionDi chuyển trong toàn cây; khi chọn message của người dùng hoặc message tùy chỉnh, Pi chép văn bản vào trình soạn thảo và chuyển đến parent
/forkFile session mớiChọn message cũ của user, trích đường dẫn đứng trước nó rồi đưa văn bản đã chọn vào trình soạn thảo
/cloneFile session mớiChép branch đang hoạt động
/resumeMở file đã cóDùng bộ chọn session của dự án
/newCấp session mớiBắt đầu với header mới và leaf null

createBranchedSession(leafId) trích một đường dẫn từ root đến leaf. Nó bỏ các nút LabelEntry khỏi đường dẫn được chép, nối lại các entry còn giữ, rồi append bản ghi label cho những đích còn tồn tại. Header mới ghi file cũ vào parentSession. Chính manager chuyển sang session mới; phương thức này không phải một hàm xuất dữ liệu thuần túy.

Gắn bản tóm tắt branch tùy chọn

Khi /tree rời một đường dẫn để sang đường dẫn khác, AgentSession.navigateTree() có thể tóm tắt đoạn đuôi bị bỏ. Nó tìm tổ tiên chung sâu nhất giữa đường dẫn cũ và đường dẫn đích, thu các entry từ nút con của tổ tiên đó đến leaf cũ, rồi sinh bản tóm tắt trước khi chuyển leaf.

Đây là mã giả về vòng đời, không phải chữ ký của một phương thức bất đồng bộ duy nhất:

fromExtension = false
result = await generateBranchSummary(entriesToSummarize, {
  model: requestModel, apiKey, headers, env, signal,
  customInstructions, replaceInstructions, reserveTokens,
  streamFn, retry, callbacks
})
summaryText = result.summary
summaryDetails = { readFiles: result.readFiles || [],
                   modifiedFiles: result.modifiedFiles || [] }
summaryUsage = result.usage
summaryId = session.branchWithSummary(newLeafId, summaryText, summaryDetails,
                                      fromExtension, summaryUsage)

Bản thân branchWithSummary() chạy đồng bộ. Nó ghi leaf cũ vào fromId, chuyển tới đích, append BranchSummaryEntry làm nút con của đích rồi đặt entry tóm tắt làm leaf mới:

e2 user: salt verification
├─ e3 ... e5 hướng đã bỏ
└─ bs1 branch_summary(fromId: "e5")  <- leafId
   └─ entry được append tiếp theo

Bộ tóm tắt mặc định làm việc trong ngân sách token, giữ các message hợp lệ gần nhất, mang theo chi tiết thao tác file và biến compaction hoặc bản tóm tắt branch cũ thành context tóm tắt. Nó bỏ toolResult thô khỏi lời nhắc tóm tắt branch vì lời gọi Tool của assistant đã chỉ ra thao tác. Extension có thể hủy điều hướng, thay bản tóm tắt, chỉnh chỉ dẫn hoặc cung cấp dữ liệu chi tiết.

Nếu không có bản tóm tắt, thao tác điều hướng gọi branch() hoặc resetLeaf(). Nếu có bản tóm tắt, các entry bị bỏ vẫn nằm trong file, còn bản tóm tắt ngắn đi vào branch đích dưới dạng BranchSummaryMessage riêng. Không branch nào bị gộp hay xóa.

5. Dựng lại context đang hoạt động

Dữ liệu lưu có dạng cây, còn Agent và bộ chuyển đổi của provider nhận AgentMessage[] tuyến tính. Coding Agent thực hiện một phép chiếu rõ ràng, thay vì xem thứ tự dòng JSONL là context của model.

Vì sao phép chiếu là một bước riêng

Thứ tự vật lý trả lời “bản ghi này được append lúc nào?”. Thứ tự parent trả lời “lịch sử nào thuộc vị trí này?”. Sau khi phân nhánh, hai thứ tự khác nhau. Gửi mọi dòng vật lý sẽ trộn các hướng cạnh tranh, bản ghi label và trạng thái của branch mà người dùng đã rời.

getBranch() cung cấp path đầy đủ từ root đến leaf. buildContextEntries() áp dụng compaction mới nhất trên path đó. buildSessionProjection() áp dụng context_edit mới nhất cho từng target đã chọn mà không sửa raw entry. Sau đó, buildSessionContext() trả projected message rồi resolve model cùng thinking state từ path đầy đủ.

Bước 1: đi từ leaf về root

Mã giả bám theo mã nguồn này phản ánh hàm hỗ trợ nội bộ buildSessionPath():

if (leafId === null) return []
leaf = leafId ? byId.get(leafId) : entries.at(-1)
path = []
while (leaf exists):
  path.push(leaf)
  leaf = leaf.parentId ? byId.get(leaf.parentId) : undefined
return path.reverse()

Với hướng thứ hai đã hoàn tất, đường dẫn được chọn là:

[e1, e2, e6, e7, e8, e9, e10]

Đoạn đuôi e3 -> e5 không xuất hiện. Nếu parent bị gãy, quá trình duyệt dừng sớm vì phép tra cứu trả về undefined; SessionManager không tự dựng phần lịch sử đã mất.

Bước 2: chiếu từng kiểu entry đã chọn

Sau bước chọn theo compaction, sessionEntryToContextMessages() phân nhánh xử lý như sau:

message          -> AgentMessage đã lưu
custom_message   -> CustomMessage
branch_summary   -> BranchSummaryMessage
compaction       -> CompactionSummaryMessage
context_edit      -> không sinh message; sửa target đã chọn trong projection
model_change     -> không sinh message
thinking change  -> không sinh message
usage            -> không sinh message
custom           -> không sinh message
label            -> không sinh message
session_info     -> không sinh message

Chuỗi message đang hoạt động của ví dụ có dạng:

[
  UserMessage(e2),
  UserMessage(e6),
  AssistantMessage(e7: lệnh gọi grep + read),
  ToolResultMessage(e8: grep),
  ToolResultMessage(e9: read),
  AssistantMessage(e10: chẩn đoán mới)
]

Hai user message đứng liền nhau vì ví dụ gọi branch("e2") rồi append bên dưới. Bước chuẩn hóa cho từng provider diễn ra sau tại convertToLlm; phép chiếu session giữ nguyên lịch sử Agent message thay vì đoán quy tắc định dạng giao tiếp của provider.

Xác định model và trạng thái suy luận

Bước lấy trạng thái đi qua toàn bộ đường dẫn đã chọn từ root đến leaf. thinkingLevel bắt đầu bằng "off"; mỗi thinking_level_change ghi đè giá trị. model bắt đầu bằng null; mỗi model_change ghi đè nó, và một message của assistant cũng cập nhật model từ provider cùng model đã tạo phản hồi đó.

Với Virtual Model đã chọn, entry model_change là session/tree state của selection đó, còn router state được lưu riêng trong một custom entry có tên pi.virtual-model-state. Khi restore hoặc chọn branch, Pi dựng lại selection đã đăng ký từ active path; nếu Virtual Model không còn đăng ký, Pi fallback về physical response mới nhất trên branch. Physical model dùng cho mỗi assistant turn được ghi trong assistant message. Compaction thay đổi canonical context projection; nó không replay routing decision cũ và không reroute turn đã lưu.

e1 model_change anthropic/claude-sonnet-4-6 -> model = cặp đó
e7 assistant từ cùng cặp                         -> model = cặp đó
path không có thinking_level_change              -> thinkingLevel = "off"

Entry hợp lệ cuối cùng trên đường dẫn có hiệu lực. Khi di chuyển leaf về trước một entry thay đổi trạng thái, thay đổi đó rời khỏi đường dẫn. Lưu lần chuyển trạng thái thành entry vì thế giúp rewind phục hồi trạng thái lịch sử mà không sửa một biến toàn cục của session.

Áp dụng CompactionEntry mới nhất

buildContextEntries() tìm CompactionEntry cuối cùng trên đường dẫn đang hoạt động. Coding Agent v3 hiện tại lưu firstKeptEntryId; compaction task riêng của Pi Durable dùng quy ước entry và cut point khác.

e1 user: yêu cầu cũ
e2 assistant: phần việc cũ
e3 user: yêu cầu gần đây          <- firstKeptEntryId
e4 assistant: phần việc gần đây
e5 compaction(summary, firstKeptEntryId: "e3")
e6 user: công việc sau compaction

Phép chiếu đặt compaction entry trước, rồi các entry từ firstKeptEntryId đến trước compaction, rồi những entry sau compaction:

[
  CompactionSummaryMessage(từ e5),
  UserMessage(e3),
  AssistantMessage(e4),
  UserMessage(e6)
]

Các entry thô e1 và e2 vẫn còn trong JSONL. Nếu chuyển đến leaf trước e5, đường dẫn không chứa compaction đó nên các message cũ có thể xuất hiện lại. Nếu không tìm thấy firstKeptEntryId trên đường dẫn đã chọn, bản triển khai hiện tại trả bản tóm tắt cùng các entry sau compaction, không tự tạo một đoạn đầu được giữ lại.

6. Lưu cây bằng JSONL

Định dạng file đơn giản, nhưng hai miền timestamp và chính sách tạo file muộn cần được xử lý chính xác.

Lưu một bản ghi trên mỗi dòng

Dòng đầu là header v3. Mỗi dòng sau là một SessionEntry. Ví dụ này là JSONL hợp lệ; từng dòng vật lý được phân tích cú pháp độc lập:

{"type":"session","version":3,"id":"01992742-9d1a-7aa0-b123-112233445566","timestamp":"2026-08-24T10:00:00.000Z","cwd":"/work/auth"}
{"type":"model_change","id":"a1b2c3d4","parentId":null,"timestamp":"2026-08-24T10:00:05.000Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2026-08-24T10:23:00.000Z","message":{"role":"user","content":"Vì sao salt verification thất bại trong src/auth.ts?","timestamp":1787566980000}}
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2026-08-24T10:23:30.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Tôi sẽ xem hàm verification."},{"type":"toolCall","id":"call_001","name":"read","arguments":{"path":"src/auth.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":1250,"output":80,"cacheRead":0,"cacheWrite":0,"totalTokens":1330,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1787567010000}}
{"type":"message","id":"d4e5f6a7","parentId":"c3d4e5f6","timestamp":"2026-08-24T10:23:31.000Z","message":{"role":"toolResult","toolCallId":"call_001","toolName":"read","content":[{"type":"text","text":"export function verifySalt(...) { ... }"}],"isError":false,"timestamp":1787567011000}}
{"type":"message","id":"e5f6a7b8","parentId":"d4e5f6a7","timestamp":"2026-08-24T10:24:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Phép so sánh dùng sai encoding."}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":1400,"output":40,"cacheRead":0,"cacheWrite":0,"totalTokens":1440,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1787567040000}}
{"type":"message","id":"f6a7b8c9","parentId":"b2c3d4e5","timestamp":"2026-08-24T10:30:00.000Z","message":{"role":"user","content":"Hãy xem bản triển khai của hàm hash trước.","timestamp":1787567400000}}

Mặc định, thư mục là ~/.pi/agent/sessions/--<encoded-cwd>--/. newSession() đặt tên file theo mẫu <ISO-with-colons-and-dots-replaced>_<sessionId>.jsonl. Session ID mặc định là UUIDv7. Mã gọi có thể cấp ID chứa chữ số, chữ cái, ., _ và -, với ký tự đầu và cuối là chữ hoặc số.

ID của entry lấy tám ký tự đầu của UUID ngẫu nhiên, thử tối đa 100 lần để tránh trùng trong byId, rồi dùng UUID đầy đủ nếu vẫn trùng. Timestamp của entry là chuỗi ISO. Timestamp lồng trong Pi AI message là số Unix mili giây. parentSession xuất hiện trong header được tạo từ session khác qua fork, clone hoặc newSession({ parentSession }).

JSONL giúp thao tác ghi phổ biến chỉ append một dòng thay vì tuần tự hóa lại một mảng JSON ngày càng lớn. Nó cũng để lộ branch: hai bản ghi có cùng parentId là các nút cùng cha dù giữa chúng có nhiều dòng vật lý.

Trì hoãn tạo file đến khi hội thoại bắt đầu

Việc tạo SessionManager hoặc session object chỉ cấp path dự kiến và có thể append setup entry, nhưng không tạo session file. Trong prompt flow thông thường, session file được tạo khi user message đầu tiên được append, trước khi provider tạo assistant response. Bên trong, _hasConversation() chấp nhận cả user message lẫn assistant message, nên host append trực tiếp một assistant message cũng vượt qua boundary này. Chính sách trong _persist() là:

Có user hoặc assistant trong fileEntriesflushedHành vi ghi
ChưafalseGiữ setup entry trong bộ nhớ; chưa tạo file
ChưatrueAppend entry hiện tại, trường hợp biên sau khi mở file đã được ghi
CófalseMở đường dẫn mới bằng "wx", ghi header và mọi entry trong vùng đệm, rồi đặt flushed
CótrueChỉ append entry hiện tại bằng appendFileSync

Việc trì hoãn ngăn thao tác mở rồi đóng Pi mà không chat tạo ra file chỉ có setup. Ghi từ user message đầu tiên giúp giữ lại prompt khi turn đầu không bao giờ tạo assistant response. Điều đó không bảo đảm mọi turn đã lưu luôn hoàn chỉnh: sự cố process hoặc lỗi provider vẫn có thể để lại user message chưa có assistant response theo sau.

isPersisted() cho biết manager có được cấu hình để lưu bền vững hay không. Nó không kiểm tra cơ chế tạo muộn đã tạo file chưa. Vì vậy, một manager lưu bền vững có thể trả về đường dẫn dự kiến từ getSessionFile() dù đường dẫn đó chưa tồn tại.

Xử lý rõ việc ghi lại file, phần cuối dở dang và bản sao lưu

Phần lớn thao tác là append, nhưng SessionManager của Coding Agent cũng ghi lại toàn bộ file trong các trường hợp sau:

  • Khi đọc session v1 hoặc v2, Pi nâng lên v3 và gọi _rewriteFile() trên cùng đường dẫn.
  • Khi mở một file rỗng được chỉ định trực tiếp, Pi khởi tạo rồi ghi lại file đó.
  • createBranchedSession() tạo file mới từ một đường dẫn và các label đã xác định.
  • forkFrom() tạo header mới bằng "wx" rồi append mọi entry không phải header từ nguồn.

_rewriteFile() mở file đích bằng "w", cắt ngắn rồi ghi từng dòng trong vòng lặp. Nó không ghi trước vào file cùng thư mục, không đổi tên nguyên tử và không tạo bản sao lưu. Hãy sao chép session có giá trị trước khi nâng phiên bản hoặc biến đổi hàng loạt. “Cây chỉ ghi thêm” mô tả mô hình entry logic, không cam kết file vật lý không bao giờ bị ghi lại.

Bộ nạp v3 đọc UTF-8 theo từng khối, phân tích từng dòng đầy đủ và âm thầm bỏ dòng lỗi. Nó cũng thử phân tích đoạn cuối không có ký tự xuống dòng rồi bỏ qua nếu thất bại. Bộ nạp kiểm tra entry hợp lệ đầu tiên là session header có ID dạng chuỗi, nhưng không báo mọi dòng lỗi hay sửa phần cuối bị ghi dở. Trình phân tích dùng cho kiểm toán hoặc chuyển đổi nên chặt hơn hành vi khôi phục khi chạy này.

Manager của Coding Agent gọi API file đồng bộ và không có khóa file, fsync, compare-and-swap hay hàng đợi ghi xuyên tiến trình. Mỗi file session chỉ nên có một tiến trình ghi. list() và listAll() chỉ đọc; chúng nạp siêu dữ liệu của tối đa mười file cùng lúc, bỏ file không đọc được rồi sắp xếp theo thời điểm hoạt động suy ra.

Storage của Pi Durable có quy ước commit và recovery riêng. Các quy ước đó không áp dụng cho SessionManager v3 của Coding Agent; cần đánh giá từng bản triển khai ở đúng ranh giới lưu trữ.

Báo lỗi mà không mặc định transcript là dữ liệu công khai

/bug [description] chuẩn bị một bundle chẩn đoán riêng tư cho đội ngũ phát triển Pi. Bundle thu thập metadata về environment, model, provider, Extension và setting sau khi che secret, cùng các chẩn đoán từ assistant message và error đã ghi nhận. Redaction giảm nguy cơ lộ dữ liệu ngoài ý muốn, nhưng bạn vẫn cần kiểm tra report vì prompt và Tool output có thể chứa giá trị nhạy cảm không có hình dạng credential quen thuộc.

Flow xác nhận cho phép kèm hoặc bỏ transcript của session. Nếu bỏ transcript, bạn có thể yêu cầu model hiện tại viết một summary thay thế. Việc tạo summary sẽ gửi các message được chọn từ transcript tới provider của model hiện tại, dùng credential và token của bạn. Pi đính kèm summary đã tạo vào bug report, không đính kèm transcript. Hãy đọc transcript hoặc summary do model tạo trước khi chia sẻ. Report có thể được upload lên Radius hoặc xuất thành ZIP local để tự kiểm tra và chuyển giao. Trong offline mode, Pi vẫn cho xuất ZIP local nhưng không thể upload report lên Radius.

Ranh giới chẩn đoánChế độKết quả
metadata.secretsallredacted
transcriptconsent: includeattached
transcriptconsent: omitnot attached
model summary requestconsent: model-writtentranscript sent to current provider
bug-report attachmentmodel-written summarysummary attached; transcript not attached
Radius uploadonlineallowed
Radius uploadofflineblocked
local ZIPonlineallowed
local ZIPofflineallowed
crash metadataall~/.pi/agent/crashes.json

Radius upload không yêu cầu login; Radius session đã xác thực sẽ gắn report với tài khoản để maintainer có thể follow up. Sau khi upload, Pi ghi report ID vào session dưới dạng entry pi.bug-report. Process crash được ghi riêng ở ~/.pi/agent/crashes.json, thông báo một lần trong lần khởi động kế tiếp và đính kèm vào report tiếp theo. Crash file đó là diagnostic state, không thay thế session JSONL.

Các bản sửa độ tin cậy của session được giữ trong Pi 1.0.0

Bốn fix làm chặt các workflow cụ thể mà không đổi storage model. Imported JSONL trùng filename với destination đã có giờ nhận suffix dạng số thay vì ghi đè file đó. Các thao tác share session đồng thời không ghi đè nhau nữa. Một fork nay giữ ranh giới compaction áp dụng, nên context dựng lại tôn trọng checkpoint của source. Một fork in-memory được yêu cầu trước khi active turn settle chỉ được xử lý sau khi runtime teardown đã await active response, nhờ đó giữ turn đã hoàn tất hoặc bị abort trước khi manager thay đổi.

Đây là các collision fix và ordering fix, không phải transaction layer mới. SessionManager của Coding Agent vẫn có các giới hạn về rewrite, backup và một writer ở trên; content import vẫn cần trust-boundary validation, còn share destination không phải concurrent session store tổng quát.

7. Tách quyền sở hữu runtime và dùng SessionManager

Pi 1.0.0 giữ API session của Coding Agent tại ranh giới Coding Agent. Pi Durable là nền tảng thử nghiệm riêng với conversation và Documents của nó:

Thuộc tínhSessionManager của Coding AgentNền tảng Pi Durable thử nghiệm
Lớp trừu tượng công khaiLớp cụ thể đồng bộCác quy ước Harness, Conversation, Session và Storage
Kiểu entry hiện tại11 kiểu SessionEntry, gồm usage, context edit, label và thông tin sessionConversation entry cùng Documents có địa chỉ riêng
Lược đồ lưu trữHeader JSONL v3 type: "session", timestamp entry dạng ISODurable entry, task, submission và document revision
Bản triển khai trên fileGọi fs trực tiếp trong SessionManagerChọn bản triển khai Storage cho Durable session
Bản triển khai trong bộ nhớSessionManager.inMemory()MemoryStorage và createSession()
Tầng lưu trữ tùy chỉnhKhông có điểm để truyền storage adapterTriển khai Storage của Durable; không phải adapter của SessionManager

Agent công khai của Agent Core vẫn là vòng thực thi trong bộ nhớ ở tầng thấp. Coding Agent sở hữu SessionManager, AgentSession và AgentSessionRuntime, gồm phép chiếu session và vòng đời Extension được mô tả ở đây. Server hoặc host bên ngoài có thể dùng runtime API được hỗ trợ của Coding Agent và tự lưu FileEntry[]; không thể truyền Storage của Pi Durable vào SessionManager. Conversation, compaction task và Documents của Durable tuân theo quy ước riêng và không thay thế hướng dẫn session này.

SessionManager vẫn hữu ích bên ngoài CLI vì gói có xuất lớp này cùng các kiểu entry/context. Các phương thức tĩnh chính gồm:

Phương thức tĩnhHành vi
create(cwd, sessionDir?, options?)Tạo session lưu bền vững mới cùng đường dẫn file dự kiến
open(path, sessionDir?, cwdOverride?)Đọc một file, nâng phiên bản cũ, dựng chỉ mục và dùng cwd trong header nếu không thay thế
continueRecent(cwd, sessionDir?)Mở session phù hợp gần nhất hoặc tạo session mới
inMemory(cwd?, options?, entries?)Dùng cùng hành vi tree mà không có file; có thể khôi phục FileEntry[] bên ngoài
forkFrom(sourcePath, targetCwd, sessionDir?, options?)Chép entry không phải header của nguồn dưới header v3 mới có parentSession
list(cwd, sessionDir?, onProgress?)Trả siêu dữ liệu session của dự án theo thời điểm hoạt động mới nhất trước
listAll(onProgress?) hoặc listAll(sessionDir?, onProgress?)Tìm qua mọi thư mục dự án đã mã hóa hoặc một thư mục được cấp

API của đối tượng chia thành bốn nhóm:

NhómPhương thức hiện tại
Vòng đời và định danhnewSession(), setSessionFile(), createBranchedSession(), isPersisted(), usesDefaultSessionDir(), getCwd(), getSessionDir(), getSessionId(), getSessionFile()
AppendappendMessage(), appendThinkingLevelChange(), appendModelChange(), appendUsage(), appendCompaction(), appendContextEdit(), appendCustomEntry(), appendCustomMessageEntry(), appendLabelChange(), appendSessionInfo()
Tree và labelgetLeafId(), getLeafEntry(), getEntry(), getChildren(), getBranch(), getTree(), getLabel(), branch(), resetLeaf(), branchWithSummary()
Phép chiếu và kiểm trabuildContextEntries(), buildSessionProjection(), buildSessionContext(), getEntries(), getHeader(), getSessionName()

Mọi append method trừ appendUsage() đều trả ID của entry mới; appendUsage() trả UsageEntry vừa tạo. appendCompaction() và branchWithSummary() nhận details, fromHook cùng usage tùy chọn; việc sinh summary thuộc AgentSession, không thuộc manager. getEntries() trả array mới chứa các stored entry object, nên caller không được sửa các object đó.

Ví dụ có thể sao chép này chỉ dùng các phương thức công khai đã xuất:

import { SessionManager } from "@earendil-works/pi-coding-agent";

const session = SessionManager.create(process.cwd());
const firstQuestionId = session.appendMessage({
  role: "user",
  content: "Rà soát luồng xác thực.",
  timestamp: Date.now(),
});

session.appendThinkingLevelChange("high");
session.branch(firstQuestionId);
session.appendMessage({
  role: "user",
  content: "Bắt đầu từ bản triển khai hàm băm mật khẩu.",
  timestamp: Date.now(),
});

const { messages, model, thinkingLevel } = session.buildSessionContext();
const sessions = await SessionManager.list(process.cwd());

console.log({
  messages: messages.length,
  model,
  thinkingLevel,
  sessions: sessions.length,
});

Một số hàm đã export cho phép dùng trực tiếp cơ chế này mà không cần object SessionManager: buildContextEntries(), buildSessionProjection(), buildSessionContext(), sessionEntryToContextMessages(), parseSessionEntries(), migrateSessionEntries() và getLatestCompactionEntry(). Helper nội bộ của class như _buildIndex(), _appendEntry(), _persist() và _rewriteFile() triển khai storage policy, không phải API ổn định cho application.

Với persistence do bên ngoài sở hữu, gọi SessionManager.inMemory(cwd, { id: sessionId }, entries) bằng FileEntry[] đúng cấu trúc. Cách này khôi phục append-only tree cùng provider projection chuẩn nhưng không tạo Pi JSONL file hay ghi ngược về external store. Điều hướng AgentSession đang live qua session.navigateTree(). Sau khi append trực tiếp qua session.sessionManager, gọi session.refreshContext() để dựng lại public finalized message từ manager. Thay message array của Agent bên dưới không phải API chỉnh session context. Host sở hữu validation, migration policy, snapshot, concurrency và durable write; parseSessionEntries() bỏ qua JSON lỗi nên không phải schema validator đầy đủ, còn migrateSessionEntries() thay đổi input array.

Chi tiết khi tạo và mở session có ảnh hưởng trực tiếp đến mã gọi. create() có thể trả một manager mà getSessionFile() mới chỉ là đường dẫn dự kiến, còn newSession() có thể trả trực tiếp đường dẫn đó, vì cơ chế ghi tạo file muộn. open() lấy sessionDir từ thư mục cha của file nếu mã gọi không cấp. continueRecent() lọc theo cwd trong header khi dùng thư mục tùy chỉnh dùng chung. list() và listAll() trả siêu dữ liệu SessionInfo, không trả manager đang mở; hãy gọi open(info.path) để tiếp tục.

8. Mang thiết kế sang hệ thống khác

Session Tree giải bài toán cụ thể của CLI cục bộ, nhưng các câu hỏi thiết kế của nó dùng được ở nhiều nơi.

Tách nơi lưu trữ khỏi cấu trúc

Quyết địnhCâu hỏiCâu trả lời của Coding Agent
Nơi lưuDữ liệu bền vững được ghi ở đâu và bằng cách nào?Một file JSONL cục bộ cho mỗi session, nhóm theo cwd đã mã hóa
Cấu trúcCác lịch sử liên hệ ra sao?Entry lịch sử không đổi, mỗi entry nối tới một parent
Lựa chọnLịch sử nào đang hoạt động?Một leaf trong bộ nhớ cùng đường dẫn về root
Phép chiếuNội dung nào đến model?Entry đã áp dụng compaction được đổi thành AgentMessage[]

Dịch vụ có thể giữ cùng cấu trúc nối bằng parent trong SQL. Một bài kiểm thử nhỏ có thể giữ toàn bộ trong bộ nhớ. Cây không phụ thuộc JSONL, và JSONL không bắt buộc dữ liệu phải là cây.

Dùng lịch sử chỉ ghi thêm để rewind và so sánh

Liên kết parent giữ các lần thử cũ mà không sao chép chúng trong cùng session. Đổi lại, hệ thống giữ nhiều dữ liệu hơn và một số dạng xem cần quét tuyến tính. Lợi ích là lịch sử có thể kiểm tra và con trỏ di chuyển rẻ.

Hãy định nghĩa độ bền dữ liệu riêng. Coding Agent chỉ lưu branch khi entry mới trỏ đến parent đã chọn. Hệ thống cần lưu chính thao tác điều hướng phải có bản ghi con trỏ rõ ràng hoặc một kho con trỏ có giao dịch.

Lưu thay đổi trạng thái tại nơi nó có hiệu lực

Thay đổi model và mức suy luận thuộc về đường dẫn lịch sử vì ý nghĩa của chúng phụ thuộc vị trí. Rewind phải phục hồi thiết lập đã áp dụng ở đó. Coding Agent còn dùng trường provider/model trong message của assistant khi dựng lại trạng thái model, nhờ vậy đường dẫn được tiếp tục sẽ khôi phục model đã sinh phản hồi gần nhất của assistant.

Có thể áp dụng cùng phép thử cho trạng thái khác: nếu chuyển tới nút cũ phải khôi phục một giá trị, hãy ghi thay đổi thành sự kiện trên đường dẫn. Với dữ kiện có nghĩa trên toàn session, chẳng hạn tên hiển thị mới nhất, hãy tách nó khỏi quá trình dựng lại theo branch.

Bản bàn giao cho lần rà soát triển khai cần cụ thể: chốt quy ước của header và entry, quy tắc lưu con trỏ hiện tại, phân biệt chọn branch với trích branch, mô tả phép chiếu, quyết định bản tóm tắt đi vào context ra sao, rồi nêu rõ bảo đảm về việc ghi lại file, bản sao lưu và truy cập đồng thời. Chỉ một sơ đồ cây không trả lời được sáu quy ước đó.

9. Tiếp tục từ ranh giới session

Từ Chương 3 đến Chương 10, đường chạy đã nối liền: vòng lặp phát message, Tool thêm cặp lời gọi/kết quả, kỹ thuật context giới hạn đầu vào, compaction append mốc tóm tắt, còn phép chiếu session chọn branch cho lần gọi model tiếp theo.

Hệ thống Extension của Pi nằm ở cả hai phía ranh giới này. Extension có thể append trạng thái custom, đưa context custom_message vào, cung cấp bản tóm tắt compaction hoặc branch, gắn label cho entry và quan sát điều hướng. Các file mã nguồn nên đọc tiếp gồm:

Chương này bám theo Pi 1.0.0 tại commit a13d35a742c6ef8462812a28fbe1d8c8b7431c32.

Trong trang này

1. Bắt đầu từ hai câu hỏi về lưu trữCâu hỏi A: session được lưu ở đâu?Câu hỏi B: session có cấu trúc gì?2. Xây Session Tree theo từng bướcBước 1: đổi model và tạo root entryBước 2: append user messageBước 3: lưu lời gọi Tool của assistantBước 4: nối kết quả Tool với lời gọi tương ứngBước 5: append chẩn đoán đầu tiênBước 6: rewind bằng cách di chuyển leafBước 7: append bên dưới vị trí đã rewindBước 8: tiếp tục branch mới3. Đọc một session entryMột SessionMessageEntry đầy đủMười một loại entry của Coding AgentVì sao entry chỉ lưu parent4. Append, rewind, branch và tóm tắtThao tác 1: append một nút conThao tác 2: rewind con trỏ hiện tạiThao tác 3: append sau khi rewindGắn bản tóm tắt branch tùy chọn5. Dựng lại context đang hoạt độngVì sao phép chiếu là một bước riêngBước 1: đi từ leaf về rootBước 2: chiếu từng kiểu entry đã chọnXác định model và trạng thái suy luậnÁp dụng CompactionEntry mới nhất6. Lưu cây bằng JSONLLưu một bản ghi trên mỗi dòngTrì hoãn tạo file đến khi hội thoại bắt đầuXử lý rõ việc ghi lại file, phần cuối dở dang và bản sao lưuBáo lỗi mà không mặc định transcript là dữ liệu công khaiCác bản sửa độ tin cậy của session được giữ trong Pi 1.0.07. Tách quyền sở hữu runtime và dùng SessionManager8. Mang thiết kế sang hệ thống khácTách nơi lưu trữ khỏi cấu trúcDùng lịch sử chỉ ghi thêm để rewind và so sánhLưu thay đổi trạng thái tại nơi nó có hiệu lực9. Tiếp tục từ ranh giới session