Dayward AI
Week 1 · D6About 6 hours

Messages, Context Engineering and Compression, Session Storage/Recovery/Forking (dg M06/M08/M09/M10)

Understand how messages are organized and passed around inside an agent, how to compress context once the window fills up, and how a session is persisted, recovered, and forked.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Draw a message's complete path from user input to being written into history
  2. Implement a simple context-compression strategy that fires automatically as the window nears full
  3. Implement persisting a session, resuming it to continue the conversation, and forking a new session from a given step

Tools learned to correct themselves yesterday, but D5 left a bill open: every tool call pushes two messages into messages, and the tool-result one is often a thousand-token blob of JSON. The better your tools, the faster the history grows, until it bursts the context window. Today we face that head on.

Plain-Language Walkthrough

The life of a message: an agent's memory is this array

Some classrooms keep a relay diary: one notebook circulates, and when your turn comes you have to page back through it to know what to write next. The thicker it gets the longer the paging takes, and you have no alternative. An agent's memory is that notebook — D1 established that the model is stateless and that a multi-turn conversation is entirely the messages array you maintain client-side. Today we fill in its internal structure. Every message carries a role; D1 covered three (system, user, assistant), and D2's tools added a fourth, tool, which holds a tool's return value.

So a message's path runs: user input arrives and appends a user; the array goes to the model; the model decides to call a tool and the returned assistant carries tool_calls, appended as-is; the tool runs and its result is wrapped into a tool and appended; the array is resent, and only now does the model emit prose for the user, which is another assistant to append. One round with a tool in it writes four messages into the history, not one.

messages.js
// The agent's "memory" is this array: the whole of it is resent to the model every round
const history = [
  { role: 'system', content: 'You are an order assistant.' },
  { role: 'user', content: 'When will the order I placed three days ago arrive?' },
]
 
// One tool call pushes two messages: the model's call request plus the tool's result
function appendToolRound(history, call, result) {
  history.push({ role: 'assistant', content: '', tool_calls: [call] })
  history.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) })
}
 
// A rough token estimate: one character counts as one token, deliberately on the high side.
// Underestimating means the compression threshold never fires, and that is the one
// mistake you cannot make here
const estimateTokens = (msgs) =>
  msgs.reduce((sum, m) => sum + (m.content ?? '').length + 4, 0)

That estimateTokens deserves a note. D1 put an English word at roughly 1.3 tokens, so counting characters directly lands above the true figure — and the direction is a deliberate choice. Underestimate and real usage hits 100% while your number has only just passed 70%, the threshold never fires, and you find out via a hard error thrown back by the model.

Run the arithmetic and D5's warning gets concrete. One order-table lookup returns two or three thousand tokens; assume one tool call per round with an 800-token result, and with the prose on both sides a round books about 1,000 tokens. Thirty rounds is 30,000, and all of it is resent in full on round 31. What bursts a history is never what the user typed, it is what tools hand back to the model — precisely the part that grows faster the better you make the agent. So the question is not whether it fills up, it is what to do when it does.

Context engineering: deciding what goes in a fixed budget

D1 used a desk to explain what a context window is; today we go straight to the next question — the desk is this big, so what happens when it is full? Change the scene: packing a cabin bag before a trip. The volume is fixed, and clothes, laptop, and papers all want to come, so packing is not about how much fits but about ranking priorities inside a fixed volume while leaving room for what you buy on the way back. Context engineering is exactly that: you decide what goes into each request, and how much.

The budget splits into at least four parts, and most people only count one:

ConsumerWhat decides its sizeTypical magnitude
System promptthe block you wrote (D4's persona plus scope plus output requirements plus dynamic context)300 to 800 tokens
Tool definitionshow many tools you registered and how long each description and schema isabout 1,000 to 1,500 tokens for 10 tools
Conversation historyhow many rounds, and how much tools returnedunbounded
Output reservethe most you want the model to say this timeusually 1,000 to 4,000 tokens

The second row is the one people forget. Tool definitions are resent every round, billed by volume just like the history, and they silently get longer the day you register your eleventh tool. That is the other face of D5's principle: the more detail you write, the more accurately the model uses a tool, and every word is paid for once per round.

How the context window gets filled up1/5
Used 20 / 100 tokens
system persona20 tok
The context window is the model's desk — a fixed size. The system persona goes on first, and it usually has to stay there the whole time.

So a 128k window never means you have 128k available. Subtract the first three rows and what is genuinely left for the history is often only six or seven tenths — and you must keep a safety margin, because hitting the ceiling returns a hard error from the model and the user sees outright failure rather than slightly worse recall. The move is to give the history its own budget line and act the moment it is touched. So, act how?

Context compression: keep the decisions, drop the transcript

Nobody sends the verbatim transcript of a two-hour meeting to the group chat. They send minutes: who owns what, due when, which points went unresolved. The ten minutes that went off-topic should be thrown away the moment the meeting ends — what gets used later is never the process, it is the conclusion.

Context compression is minute-taking for a conversation, and it answers three questions: when to fire, what to drop, what to keep.

When to fire. Use a threshold, not a timer, and never wait for an error. The common practice is to fire once the history exceeds seven tenths of its budget. Why not nine tenths? Compression itself is a model call to write the summary, so it has latency and it can fail; leave it until nine tenths and one timeout or rate limit means the very next round hits the wall. Seven tenths is the rescue window you leave yourself.

What to drop. Prioritize the process-shaped material in the earliest stretch: the model's intermediate reasoning, raw tool results that have already been consumed, requirements the user later overruled. What they share is that their value has already settled into the conclusions that follow, so keeping the originals is just weight.

What to keep. This is the part that, written wrong, gives the model instant amnesia. The system prompt is always kept verbatim (it is not part of the history). The most recent handful of messages keep their original text, because the model leans hardest on what just happened. And constraints or facts the user stated ("I am in Shanghai," "budget under two thousand") must be written into the summary rather than discarded as process. One more thing goes wrong constantly: the cut has to land at the start of a round. Cut between an assistant carrying tool_calls and its matching tool result and the next request contains a dangling call with no result, which nearly every vendor's API answers with a 400.

compress.js
const MAX_TOKENS = 8000 // the history's own budget, not the model's window ceiling
const TRIGGER_RATIO = 0.7 // compress at seven tenths, leaving rescue room if the summary call fails
const KEEP_RECENT = 6 // the last 6 keep their original text: the model leans on what just happened
 
// Walk back to the nearest position that is not a tool result, so the cut lands at a round's start
function alignToTurnStart(msgs, index) {
  let i = index
  while (i > 0 && msgs[i].role === 'tool') i--
  return i
}
 
async function compressIfNeeded(history, summarize) {
  if (estimateTokens(history) < MAX_TOKENS * TRIGGER_RATIO) return history
 
  const system = history.filter((m) => m.role === 'system')
  const rest = history.filter((m) => m.role !== 'system')
  const cut = alignToTurnStart(rest, Math.max(0, rest.length - KEEP_RECENT))
  if (cut === 0) return history
 
  // The summary is itself a model call: it costs money and takes time, so do not
  // leave it until the last moment
  const digest = await summarize(rest.slice(0, cut))
  return [...system, { role: 'user', content: `[Summary of earlier conversation] ${digest}` }, ...rest.slice(cut)]
}

Here is the effect. Take that 30-round, 30,000-token example with no compression along the way. Afterwards you hold one roughly 600-token summary plus the last 6 messages in full. By this section's own arithmetic, 4 messages per round is about 1,000 tokens, so 6 messages is a round and a half, roughly 1,500. Total about 2,100 — a fourteenth of the original. And the saving is continuous: every subsequent round is billed at the compressed length, so you save not once but on all remaining rounds. The price is one extra model call plus one irreversible loss of information.

Session persistence: a receipt or a monthly statement

A supermarket receipt prints line after line and never erases a mistake to reprint it; a bank's monthly statement overwrites one fresh summary each month. Saving a session is a choice between those two. Append-only is the receipt: every message produced appends one line to the end of a file, and history is never modified. A snapshot is the statement: at the end of every round the whole session is overwritten.

Prefer append-only, for three fairly concrete reasons: write cost is independent of history length; it can naturally be replayed to any step, which is the foundation for the next two sections on recovery and forking; and when something goes wrong you have a complete audit trail. A snapshot's value is speed — replaying thousands of messages in full is too slow — so the common combination is append-only as the source of truth plus periodic snapshots.

For format, JSONL with one JSON object per line is the least trouble: readable, tail-able, and appendable without parsing the whole file. Metadata (who the parent session is, which message it forked at, how far compression has reached) goes in a small separate file:

TextText
.sessions/
  s-8f3a.jsonl        # one message per line, append-only, never rewritten
  s-8f3a.meta.json    # session metadata: id, parentId, forkedAt, compression watermark
TextText
{"seq":1,"role":"user","content":"When will the order I placed three days ago arrive?","ts":"2026-09-04T10:00:00Z"}
{"seq":2,"role":"assistant","content":"","tool_calls":[{"id":"c1","name":"query_order"}],"ts":"2026-09-04T10:00:01Z"}
{"seq":3,"role":"tool","tool_call_id":"c1","content":"...order JSON...","ts":"2026-09-04T10:00:02Z"}

In production the files become a database (a sessions table and a messages table in Postgres) with the structure unchanged.

Session recovery: load the right save point

Loading a game save, you expect to pick up exactly where you were. But if the save recorded your position and not your inventory, you reappear on the spot with nothing — the quality of a recovery is determined by what was saved in the first place. Sessions are the same, and two traps catch nearly everybody.

The first: persisting the system prompt of the moment along with the history. D4 set the rule, and the last block of its structure is dynamic context, which holds live things like the current time. A session saved three days ago and loaded today makes the model believe it is still three days ago, and it computes dates wrong. The right approach: the system prompt does not go into the persisted history. Assemble a fresh one with the current time on every recovery, then attach the conversation read off disk.

The second is subtler: the save landed in the middle of a tool call. The process died after the model returned tool_calls and before the tool result was written back, so the last line on disk is a dangling call, and sending that on the next recovery produces the previous section's 400. So after loading you must run a completeness check: if the tail is a tool_calls with no matching result, either append a tool message saying the tool execution was interrupted and to call it again, which reuses D5's error-feedback mechanism, or discard that incomplete tail.

The third thing to restore is the compression watermark: how far this session has already been compressed. Without it, the threshold check counts the already-summarized stretch again, so you either compress repeatedly or never compress at all.

Session forking: a new line from a save point

One of the great pleasures in games: save before a pivotal choice, play through option A, and if you dislike it, load and take B. Both lines share the earlier story and diverge after, and however A plays out, the save itself is unchanged.

Session forking is that, and the definition is clean: copy the first k messages of a history into a new session, remember which session is the parent and which message it was cut at; the parent is not modified in any way. That last clause is the line between forking and rolling back — a rollback lops off a stretch of history and continues, which is destructive, while a fork grows a new branch and both sides can continue.

It maps to three high-frequency product scenarios: regenerate (the user dislikes a reply and retries, which is a fork from the second-to-last message), prompt A/B experiments (attach two versions of a system prompt to the same real history and run each, which is far more credible than fabricated data), and reproducing a production issue (fork the offending session into your own account and poke at it however you like without touching the original).

fork.js
import { randomUUID } from 'node:crypto'
 
// Forking = copy the first k messages plus remember where you came from. The parent is
// read-only, which is the fundamental difference from a rollback.
function forkSession(parent, atIndex) {
  const cut = alignToTurnStart(parent.messages, atIndex)
  return {
    id: randomUUID(),
    parentId: parent.id,
    forkedAt: cut,
    // A deep copy is mandatory: referencing the parent's message objects directly means
    // the two branches will eventually contaminate each other
    messages: structuredClone(parent.messages.slice(0, cut)),
    createdAt: new Date().toISOString(),
  }
}

Two of those comments are worth reading side by side: JavaScript and Python need an explicit deep copy, because a slice copies only the outer container while the same mutable objects sit inside; Java's record and Swift's struct hold immutable or value-type elements, so copying the list is enough. That is not a syntax difference, it is a data-model difference — design messages to be immutable and forking goes from "remember to deep-copy" to "hard to get wrong."

There are three engineering costs. Storage amplification: every fork copies the first k messages in full, so five forks means six histories; at scale you switch to storing a parent reference plus a cut point and stitching on read, at the price of a more complex read path. Cost attribution: each branch spends separately, and the bill has to aggregate back up a tree by parentId, or what you see is a pile of orphaned sessions. The third is the one people miss: the session has already been compressed and the user wants to fork from a stretch that was summarized away, so the original text is gone. That is exactly why the previous callout insists the raw history be append-only. How deep you can fork depends on whether you kept the originals.

Classifying memory: what follows the session, and what follows the person

A tour guide carries two things. One is today's itinerary — meet at nine, lunch here — which is waste paper the moment the group disperses. The other is the agency's customer file, noting that this traveler avoids spice and travels with a child, still useful when they come back next year.

That is the boundary. Short-term context is this session's messages array: void when the session ends, and sent in full with every request. Everything today — compression, recovery, forking — manages that. Long-term memory is cross-session facts and preferences about a user; it does not live in messages but in an external system, retrieved on demand with the few relevant hits injected into the request.

Deciding where a piece of information belongs takes three questions: will it still be needed after this session? will it expire with time? can retrieval bring it back? "The user is in Shanghai" is needed, does not expire, and is retrievable, so it goes to long-term memory. "Rewrite that second paragraph in three sentences" is not needed later and expires immediately, so it stays short-term. Anything in between stays short-term first and settles into a long-term preference once it recurs.

The engineering differences are total: short-term context is billed by token, bounded by the window, and deleted with the session; long-term memory is stored per item, bounded by retrieval quality, and has to handle expiring an old memory when the user changes their mind. The most common misuse is stuffing long-term memory in as context all at once — six months of accumulated preferences, two hundred items, jammed into a request both bursts the window and distracts the model with irrelevant memories. The right posture is retrieving only the three to five most relevant. That retrieval machinery is RAG, D12's main subject. Today it is enough to draw the boundary: today is about this conversation remembering; D12 is about this person being remembered.

Source Reading

Hands-On Lab

🧪 D6 lab: session persistence plus automatic compression

Code location: labs/agent-30days/day-06-session-persistence

Acceptance criteria:

  1. MOCK=1 pnpm start prints a before-and-after comparison, in the form of 33 messages / 3135 tokens becoming 10 messages / 916 tokens, a 71% reduction.
  2. Run MOCK=1 pnpm start chat "..." twice in a row as two separate processes, and the second prints that it recovered N messages from disk, with N greater than 0.
  3. MOCK=1 pnpm start fork 4 forks a new session; after appending a round to it with --session=, MOCK=1 pnpm start show main shows the parent session's message count unchanged.
  4. Opening .sessions/main.jsonl shows one message per line, append-only and never rewritten, with the tool message present.
  5. pnpm typecheck passes with no any.

starter/ has four exercise points cut out of it and runs fully offline under MOCK=1: the mock layer manufactures multi-round conversations with tool calls so the history reaches the threshold quickly, and you see compression fire without paying for thirty real rounds. All four blanks default to something that runs but is visibly wrong — tokens always zero, the history never shortens, a restart reads zero messages, a fork contaminates its parent — so run it as-is once and read those four warning lines.

  1. Run MOCK=1 pnpm start unmodified first and note the four exercise-incomplete lines; that is your to-do list.
  2. Implement appendMessage persistence: append each message as JSONL to .sessions/main.jsonl, then cat the file and confirm one message per line with the tool message present.
  3. Implement estimateTokens and compressIfNeeded: once past seven tenths of the budget, summarize the early messages into one, aligning the cut with the provided alignToTurnStart, and finish with a comparison line showing the reduction.
  4. Run MOCK=1 pnpm start chat "I live in Shanghai, please note that", then start a separate process for MOCK=1 pnpm start chat "where did I say I live?"; the second must print that it recovered N messages from disk and must answer correctly.
  5. Implement the deep copy in forkSession, fork with fork 4, append a round to it with --session=, then show main and confirm the parent is untouched.

Interview Questions

Today's four questions are in the bank below, weighted toward context management, long conversations, and memory classification. Read the analysis before the key points — the follow-up on question 1, about which model should write the summary and what to do when it fails, draws the most follow-ups.

Checklist and Tomorrow

  • Draw a message's complete path from user input to being written into history
  • Implement a simple context-compression strategy that fires automatically as the window nears full
  • Implement persisting a session, resuming it to continue the conversation, and forking a new session from a given step
  • Name the four parts of the context budget, and say why the compression cut must align to a round's start
  • All 5 acceptance criteria of the lab pass
  • Answer at least 3 of the 4 interview questions without looking at the key points

Tomorrow (D7) we assemble the last six days into a real service: expose an SSE endpoint with Fastify, package it with Docker, and run the week-one retrospective. The order is deliberate — today's sessions live in a local file and survive a restart, but that road closes the instant you become a service: several instances each write their own disk, the load balancer sends the user's second request to a different machine, and the history vanishes. Session state has to move out of the process and off the single machine, and that is the first unavoidable hurdle on the way from script to service.

Interview questions

  • When a long conversation outgrows the context window, how do you compress it — when do you trigger, what do you drop, and what do you keep?长对话里上下文放不下了,你会怎么压缩?什么时候触发、压掉什么、保留什么?
    Common in ChinaCommon overseasIntermediate#context-engineering#compression#cost

    How to reason about it · think before answering

    1. Saying 'summarize it' earns nothing — everyone says that. The signal is whether you name a trigger point and a keep-list; without those you sound like someone who never ran a long conversation in production.
    2. Split it into three questions before answering: when to compress, what to drop, what to keep. The split itself scores, because it frames compression as a policy rather than a function.
    3. Trigger on a threshold, not a timer, and never on an error. Give a number and justify it: compress at roughly 70% of the history budget, because summarizing is itself a model call that can be slow or fail. Waiting until 90% means one timed-out summary call and the next turn slams into the window limit.
    4. Drop the process: intermediate reasoning, raw tool payloads already consumed, requirements the user later reversed — their value has already settled into later conclusions. Keep the system prompt (it is not history), the most recent turns verbatim, and any constraint or fact the user stated explicitly. Getting that last one wrong makes the model visibly forget.
    5. Add the detail others miss: the cut must land on a turn boundary. Slicing between an assistant tool_calls message and its matching tool result leaves a dangling call, and most providers reject that request with a 400. This is the line that proves hands-on experience.
    6. Two follow-ups to expect. Which model summarizes? A cheap small one — summarization is extraction, not reasoning, which ties back to tiered routing. And what if the summary call fails? Degrade to a plain sliding window that drops the oldest turns, so a failed compression never fails the whole turn.

    分析过程 · 先想清楚再作答

    1. 这题的区分度不在「用摘要」三个字上,几乎人人都答得出。区分度在你有没有说出触发时机和保留清单——只答「让模型总结一下前面的对话」的,面试官会判定你没在长对话上线过。
    2. 先把问题拆成三问再逐个答:什么时候压、压掉什么、保留什么。这个拆法本身就是加分项,因为它说明你把压缩当成一个策略而不是一个函数。
    3. 触发用阈值不用定时器,也不能等报错。给一个具体数字并解释它:历史占用到预算的七成就动手,因为摘要本身是一次模型调用,有延迟也可能失败,卡到九成再压,一旦摘要超时下一轮就直接撞窗口上限了——七成是留给自己的抢救时间。
    4. 压掉的是过程性内容:中间推理、已经被消费完的工具原始返回值、用户后来推翻的需求。它们的共同点是价值已经沉淀进后面的结论里。保留的是系统提示词(它不属于历史)、最近若干条原文、以及用户明确声明过的约束和事实——后者写错了模型会当场失忆。
    5. 再补一条别人不会说的:切口必须对齐到一轮的开头。切在 assistant 的 tool_calls 和对应的 tool 结果中间,下一次请求就有了悬空调用,多数厂商的 API 直接返回 400。这一条最能证明你真的调过。
    6. 可以预期的追问有两个。一是摘要该用哪个模型:用便宜的小模型就行,摘要是抽取任务不是推理任务,这也接上了 D4 的分层路由。二是摘要调用失败了怎么办:降级到不摘要的滑动窗口(直接丢最早的几轮),保证请求发得出去,别让压缩失败连带整轮对话失败。

    Key points

    • Threshold-triggered at about 70% of the history budget, because the summary call itself is a slow, fallible model call that needs headroom
    • Drop process, keep conclusions: discard intermediate reasoning and consumed raw tool payloads; keep the system prompt, the recent turns verbatim, and explicit user constraints and facts
    • Align the cut to a turn boundary — slicing between tool_calls and its tool result makes the next request fail with a 400
    • Compression is lossy and irreversible: keep an append-only original, send the compressed version, and read the original when you need to backtrack or fork
    • Summarize with a cheap small model, and degrade to a sliding window if the summary call fails so compression failure never fails the turn

    答题要点

    • 阈值触发:历史占用到预算七成就压,因为摘要本身是一次会失败、有延迟的模型调用,必须留抢救余量
    • 压过程、留结论:丢中间推理和已消费的工具原始返回,保留系统提示词、最近若干条原文、用户明确声明的约束与事实
    • 切口必须对齐到一轮开头,切在 tool_calls 与 tool 结果之间会让下一次请求返回 400
    • 压缩是有损且不可逆的:原始历史另存一份只追加,发给模型的是压缩版,需要回溯或分叉时读原始版
    • 摘要用便宜的小模型;摘要失败要能降级成滑动窗口,别让压缩失败连累整轮对话
  • What problems do session persistence, restore, and forking each solve, and what goes wrong in each?会话的持久化、恢复和分叉分别解决什么问题?实现时各有什么坑?
    Common in ChinaCommon overseasIntermediate#session-management#persistence#forking

    How to reason about it · think before answering

    1. The question lists three things side by side, so it is really testing whether you can separate their motivations. Answering 'they all save the conversation' throws away the entire signal.
    2. Give one motivation each in a sentence: persistence survives process restarts and multi-instance routing, restore lets a loaded history keep the conversation going, forking lets one history grow two different futures. Different motivations imply different data structures.
    3. The key persistence choice is append-only versus snapshot. Choose append-only and justify it: writes are independent of history length, any point can be replayed, and you keep an audit trail. Snapshots are a read optimization, so production usually means append-only as the source of truth plus periodic snapshots. This choice is what makes forking possible at all.
    4. Volunteer the two restore traps. First, persisting the system prompt inside the history: it carries dynamic context like the current time, so a session loaded three days later has the model reasoning from a stale date. Rebuild the system prompt fresh on every load. Second, a session saved mid tool call ends with an unmatched tool_calls message; replaying it verbatim gets a 400, so validate on load and either append an 'execution interrupted' tool result or drop the dangling tail.
    5. For forking, the overlooked point is that the parent stays read-only. Forking is not rollback: rollback truncates and mutates, forking copies the first k messages into a new branch and both sides continue. Deep-copy the messages — sharing the parent's objects lets the branches contaminate each other.
    6. Expect the follow-up on storage: reference the parent plus an offset and stitch on read, at the cost of a more complex read path. Add that cost must aggregate up the parentId tree, or you cannot tell which user's retry burned which tokens.

    分析过程 · 先想清楚再作答

    1. 题干把三件事并列,考的其实是你能不能分清它们各自的动机——很多人会把三个都答成「存下来」,那就丢掉了全部区分度。
    2. 先一句话各给一个动机:持久化解决「进程重启和跨机器请求」,恢复解决「加载回来还能接着聊」,分叉解决「同一段历史要走出两条不同的后续」。动机不同,所以数据结构的要求也不同。
    3. 持久化的关键选择是只追加还是快照。答只追加并给理由:写入不受历史长度影响、能回放到任意一步、有审计轨迹;快照只是读加速手段,工程上常见的是「只追加为准 + 定期快照」。这一条直接决定了分叉能不能做。
    4. 恢复的两个坑要主动说。一是把当时的系统提示词一起存进了历史,里面有「现在时间」这类动态上下文,三天后读出来模型的日期判断全错——系统提示词不进持久化历史,每次现拼。二是存档存在了工具调用中途,最后一条是没有配对结果的 tool_calls,直接发出去就是 400,加载后必须做完整性校验,补一条「执行被中断」的结果或丢弃这条尾巴。
    5. 分叉最容易被忽视的是「父会话只读」这条语义。分叉不是回滚:回滚砍掉历史继续用,是破坏性的;分叉复制前 k 条长出新枝,两边都能继续。实现上要深拷贝,直接引用父会话的消息对象会让两条分支互相污染。
    6. 可以预期的追问:分叉多了存储怎么办?答按父引用加偏移存、读时拼接,代价是读路径变复杂;再顺手补一句成本要能顺着 parentId 聚合成一棵树,否则账算不清是哪个用户的哪次重试花的钱。

    Key points

    • Persistence handles restarts and multiple instances; prefer append-only for constant-cost writes, replayability and an audit trail, with snapshots purely as a read optimization
    • On restore, rebuild the system prompt fresh — persisting the one containing the current time makes the model reason from a stale date
    • Validate on restore: a dangling tool_calls tail needs an 'interrupted' tool result or must be dropped, or the next request returns 400; restore the compression watermark too
    • A fork copies the first k messages and records parent and cut point, leaving the parent read-only — that is what separates it from destructive rollback, and it requires a deep copy
    • Forking costs storage amplification and muddled cost attribution; at scale store a parent reference plus offset and aggregate spend up the parentId tree

    答题要点

    • 持久化解决进程重启与跨实例,选只追加:写入不受历史长度影响、可回放任意一步、有审计轨迹;快照只是读加速
    • 恢复要现拼系统提示词,不能把带「现在时间」的那份存进历史,否则读出来日期判断全错
    • 恢复必须做完整性校验:尾部悬空的 tool_calls 要补一条中断结果或丢弃,否则下一次请求返回 400;压缩水位也要一起恢复
    • 分叉是复制前 k 条并记住父会话与切点,父会话只读——这是它和破坏性回滚的根本区别,实现上必须深拷贝
    • 分叉的代价是存储放大与成本归属,规模上来后改成存父引用加偏移,账要能顺着 parentId 聚合成树
  • Where do you draw the line between short-term context and long-term memory, and how do you decide where a given fact belongs?短期上下文和长期记忆的边界怎么划?一条信息该往哪放,你的判断依据是什么?
    Common in ChinaCommon overseasBasic#memory#context-engineering

    How to reason about it · think before answering

    1. This looks conceptual but is really asking for an operational test. Reciting 'short-term lives in messages, long-term lives in a vector store' just describes the status quo and gives no signal.
    2. Lay out the engineering properties and the boundary draws itself: short-term context dies with the session, ships in full on every request, is billed per token and capped by the window; long-term memory spans sessions, is retrieved and injected rather than always sent, is stored per item and capped by retrieval quality.
    3. Give a reusable test — this is the core of the answer. Ask three questions: is it still needed after this session ends, does it expire with time, can retrieval find it again? Three yeses means long-term; a no on the first means it stays short-term. Illustrate: 'the user lives in Shanghai' is long-term, 'the user just asked me to shorten that paragraph to three sentences' is not.
    4. Name the common failure: stuffing all long-term memory into the prompt. Two hundred preferences accumulated over six months will both blow the window and drown the model in irrelevance. The value of long-term memory is retrieving the three or four relevant items, not the volume stored.
    5. Expect the follow-up on updates and expiry: memories need timestamps and provenance, and a changed preference must overwrite rather than coexist with a contradictory one. Add the deletion angle — long-term memory is the part you must be able to locate and erase when a user asks for their data to be deleted.

    分析过程 · 先想清楚再作答

    1. 这题看着像概念题,其实考的是你有没有一条可执行的判据。背出「短期在 messages 里、长期在向量库里」只是描述现状,答不出「为什么这条该进长期」就没有区分度。
    2. 先把两者的工程属性摆出来,边界自然就清楚了:短期上下文随会话结束作废、全量进请求、按 token 计费、受窗口约束;长期记忆跨会话存在、不进请求而是检索后注入、按条存储、受检索质量约束。
    3. 给一条可复用的判据,这是本题的核心:问三句话——跨会话之后还需要吗、会随时间失效吗、能通过检索捞回来吗。三个都是「是」就进长期记忆,第一个是「否」就留在短期。举例说明:用户住上海进长期,用户刚才让我把段落改成三句话留短期。
    4. 点出最常见的误用:把长期记忆当上下文一次性全塞进去。用了半年攒两百条偏好,全塞进请求既撑爆窗口,又因为大量不相关记忆干扰模型判断——长期记忆的价值在于按需检索出最相关的三五条,不在于存了多少。
    5. 可以预期的追问:长期记忆怎么更新和失效?答要点是记忆要带时间戳和来源,用户改了主意要能覆盖旧记忆而不是并存两条矛盾的;再补一句删除权——用户要求删数据时,长期记忆是必须能定位并整体删掉的那一部分。

    Key points

    • Short-term context dies with the session, ships in full, and is billed per token under the window cap; long-term memory spans sessions, is retrieved on demand, and is capped by retrieval quality
    • The three-question test: is it needed after this session, does it expire, can retrieval find it — three yeses means long-term
    • The common failure is injecting the whole memory store, which blows the window and drowns the model in irrelevance; retrieve the three or four relevant items instead
    • Long-term memories need timestamps and provenance so a changed preference overwrites the old one instead of contradicting it
    • Long-term memory is the part that must be locatable and deletable per user for compliance, while short-term context simply dies with the session

    答题要点

    • 短期上下文随会话作废、全量进请求、按 token 计费受窗口约束;长期记忆跨会话、按需检索后注入、按条存储受检索质量约束
    • 判据三问:跨会话还需要吗、会随时间失效吗、能被检索捞回来吗——三个都是就进长期记忆
    • 常见误用是把长期记忆整包塞进上下文,既撑爆窗口又用不相关的记忆干扰模型,正确做法是检索最相关的三五条
    • 长期记忆要带时间戳和来源,用户改主意时覆盖旧记忆,避免两条矛盾记忆并存
    • 长期记忆是合规上必须能按用户定位并整体删除的那一部分,短期上下文随会话删除即可
  • How do context engineering and RAG relate, and what breaks if you do RAG without context management?上下文工程和 RAG 检索是什么关系?只做 RAG 不做上下文管理会出什么问题?
    Common in ChinaCommon overseasDeep dive#context-engineering#rag#retrieval

    How to reason about it · think before answering

    1. The hinge word is 'relate'. Treating them as two parallel techniques is the standard weak answer — the right frame is containment: context engineering decides what goes into this request, and RAG is one supply mechanism that fetches what should go in.
    2. Separate the responsibilities and it becomes obvious: RAG solves 'the information is neither in the weights nor in this conversation' by retrieving it; context engineering solves 'the retrieved chunks plus the history plus the tool definitions all have to fit, in some priority order'. One owns sourcing, the other owns budget.
    3. So RAG without context management breaks in three ways, best delivered in this order. Crowding: retrieved documents run to thousands of tokens and squeeze out the conversation, so the model knows the manual but forgot what the user said three turns ago. Interference: raising top-k feels safe but irrelevant chunks dilute attention and accuracy drops instead of rising. Cost: retrieved text is resent every turn, so a 2k-token passage costs ten times over ten turns.
    4. Give the correct combination: budget history and retrieval separately, keep retrieval to top-k without re-injecting the same chunks every turn, compress history when it crosses its line, and make sure both lines together still leave room for output. This 'separate budgets' framing lands much better than a vague 'you need to balance them'.
    5. Expect the follow-up on placement: putting retrieved context near the current question usually works better, and it should be labeled with its source so the model can tell reference material from what the user actually said. Note too that it is single-turn context and should not be written into the persisted history and resent forever.

    分析过程 · 先想清楚再作答

    1. 题眼在「关系」。把两者说成并列的两种技术是最常见的失分答法——正确的框架是包含关系:上下文工程是「决定这次请求里放什么」,RAG 是它的一种供给手段,负责「从外部捞该放进去的东西」。
    2. 拆开看职责就清楚了:RAG 解决的是「信息不在模型参数里、也不在当前对话里」,靠检索把它取回来;上下文工程解决的是「取回来的东西、加上历史、加上工具定义,一共放不放得下、该按什么优先级放」。前者管来源,后者管预算。
    3. 所以只做 RAG 不做上下文管理会出三类问题,最好按这个顺序说。第一是挤占:检索回来的文档动辄几千 token,直接拼进去把对话历史挤没了,模型记得住资料却忘了用户三句话前说过什么。第二是干扰:召回条数调大看着安全,实际上不相关的片段会稀释模型注意力,准确率不升反降。第三是成本:检索结果每一轮都重发,一段两千 token 的资料聊十轮就付了十次。
    4. 给出正确的组合姿势:先给历史和检索结果各划一条预算线,检索结果只保留 top-k 且不跨轮重复注入,历史超线就压缩,两条线加起来必须留出输出空间。这套「分账」的说法比笼统的「要平衡」有说服力得多。
    5. 可以预期的追问:检索结果该放在系统提示词里还是当成一条 user 消息?答放在靠近当前问题的位置通常效果更好,而且要标注来源便于模型区分「资料」和「用户说的话」;顺带说清它是一次性上下文,不该被写进长期会话历史里反复重发。

    Key points

    • They are not parallel: context engineering decides what enters the request, and RAG is one supply mechanism for information that is neither in the weights nor in the conversation
    • RAG alone crowds out history — multi-thousand-token retrievals evict the conversation, so the model knows the docs but forgot the user's last request
    • A bigger top-k is not safer: irrelevant chunks dilute attention and accuracy drops, so cap retrieval
    • Retrieved text is single-turn context; persisting it into the history means paying for it on every subsequent turn
    • Budget history and retrieval on separate lines, compress history when it crosses its line, and leave room for the output on top of both

    答题要点

    • 不是并列关系而是包含关系:上下文工程决定这次请求放什么,RAG 是给它供货的一种手段,负责把不在模型和对话里的信息检索回来
    • 只做 RAG 会挤占历史:几千 token 的检索结果把对话挤没,模型记得住资料却忘了用户刚说的话
    • 召回条数越大越准是错觉:不相关片段会稀释注意力,准确率反而下降,应控制 top-k
    • 检索结果每轮重发会持续计费,属于一次性上下文,不该写进持久化历史反复重发
    • 正确姿势是给历史和检索各划一条预算线,历史超线就压缩,两条线之外还要留出输出空间

Comments