Dayward AI
Week 1 · D2About 4 hours

The Line Between State and Context: Moving the Authoritative State Out of the Window

Fix yesterday's degradation: write the authoritative progress to disk, and after the window resets rebuild an equivalent context out of what is on disk. The point is not how to write a file. It is where that dividing line belongs, and why the goal of rebuilding is equivalence rather than restoration.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Can name the three kinds of information that must go into the on-disk state and the two kinds that belong only in the context window, with the test used to decide each
  2. Can explain how an equivalent context differs from the original context, and give a concrete case where restoring the original context makes things worse
  3. Can bring yesterday's duplicated work back by switching the state layer off, and use that mutation check to prove the state layer is really doing the work

The fix for yesterday's degradation looks obvious once you have seen it: the progress was alive in memory the whole time, and nobody put it into the opening of the next context window. So today is not about writing a file. It is about drawing three dividing lines. When you finish, scroll back up and tick off the three goals.

Plain-Language Walkthrough

A whiteboard versus what you remember

Yesterday's shift handover is not finished yet. The briefing was given exactly once. When that person went home at midnight and a third one walked in, the briefing was gone.

Every repair shop solved this long ago: there is a whiteboard on the wall. Which cars are done, which one is half apart, where the torque wrench went. Whoever walks in reads the board first.

A whiteboard differs from what a person remembers in three ways, and each one maps to a decision you make today.

  • The board outlives the shift. The person clocked out; the board is still there. That is survival across context windows.
  • The board holds conclusions, not process. Nobody writes "I tried the small wrench, it would not turn, so I got the big one." They write "car three is done." That is equivalence, not restoration.
  • There is only one board. When what you remember disagrees with the board, the board wins. That is what authoritative means.

The file you write today, work/state/run.json, is that whiteboard.

Where the line goes: authoritative on disk, volatile in the window

Be precise about what broke yesterday. Progress was never lost from memory: features[].passes survived the reset untouched. The second window lost the evidence, not the data.

So why write to disk at all? Because memory dies with the process. One kill -9, one reboot, one evicted container, and the ledger is gone. With a person watching, that costs a restart. Through the night, there is no person.

Here is the line.

Lives in the context windowLives on disk
LifetimeUntil the next compaction, truncation or resetAcross windows, processes and reboots
TrustAdvisory. It may be several steps out of dateAuthoritative. On conflict, it wins
ContentsThe context of this small slice of workWhat is done, what is in flight, how many attempts
Cost if lostRebuild oneThe whole night, and nobody knows

One more boundary, easy to skip and settled today: the state file lives outside the target repository.

In the lab the target sits in work/repo and the ledger in work/state. Two directories, deliberately. Put the ledger inside work/repo and two things follow: it lands in the agent's own commits, so a D4 verified commit carries harness bookkeeping, and rolling back to the previous verified commit rolls the ledger back with it. Rollback targets code. It should not also erase the lesson "I have already tried this three times."

Three kinds that must go on disk

Not everything deserves persisting. There is one test: what does the next context window absolutely need in order to choose the right next action? Three kinds survive it.

First, what is done. The most direct one, and the one whose absence produced yesterday's duplicated work. It maps to done: string[], in completion order, and stores feature ids rather than description text: ids are stable, descriptions drift as requirements change.

Second, what is in flight. It maps to current: string | null, and it is subtler. If the process dies halfway, disk holds "working on F07" while F07 appears neither in the done list nor in any failure record. That dangling record is useful information, not dirty data. It tells the next window that F07 may or may not have landed, so verify before assuming. Drop it and the next window either redoes finished work or skips something never finished.

Third, how many attempts. It maps to attempts: Record<string, number>. In one window, "F23 failed once" is noise. Across five, "F23 has failed twelve times" is an alarm that has to fire. The value of this class comes entirely from accumulating across windows, which you can never do without writing it down. D5 doom loop detection and the D6 budget circuit breaker both rest on this counter.

One more rides along: spent, how many windows and steps have been burned. Today it is only printed; D6 turns it into a cutoff.

Two kinds that must not

Two classes stay out, and the reason is not disk space.

First, intermediate reasoning. "I looked at the structure of server.mjs, saw that routes are registered from an array, so I am going to..." That sentence earned its keep at the time, and once the conclusion landed its job was over. Keeping it costs twice: the next window spends attention on a plan that is already void, and the state file grows into a log that gets bigger every hour. Conclusion lands, process expires.

Second, one-shot tool receipts. The output of some git status, the contents of some file you read. Their defining trait is that you can fetch them again, and the fresh copy is the correct one. Feeding a three-hour-old git status to a new window is worse than feeding nothing: it decides against a stale snapshot while believing it is current.

Rebuilding aims at equivalence, not restoration

Now the least intuitive idea of the day. The window has reset. What goes into the new opening?

The natural answer is restoration: splice the previous conversation back on. It looks lossless. It is the most expensive mistake available today.

Start with the price. In the lab, one window of three steps produces a raw transcript of 962 characters; the rebuilt summary is 175 characters. Three steps already differ by more than five times, and a full night is hundreds of thousands of tokens against a few hundred. Restoration is also not sustainable on its own terms: you switched windows because the context did not fit, so stuffing it back in blows up the second window immediately.

But cost is not the real problem. A restored context is harder to use than a summary.

Replay a night of raw transcript and the model has to re-derive, from several hundred steps, which items are actually finished. That derivation has error, has cost, and is unnecessary: the harness already holds the conclusion, sitting in done.

So rebuilding is a lossy compression that is lossless for decisions: reasoning and tool receipts are dropped, while everything needed to choose the next action survives.

The summary is written for the next window, not for a person

If you are going to summarize, be clear about who reads it.

The D7 overnight report is read by a human, so it needs narrative and readable prose. Today's text has exactly one reader: the model inside the next context window. So it is written to machine taste.

  • Conclusions first, one fact per line, no connective sentences
  • Feature ids, never human phrasing like "that endpoint for the note list"
  • An explicit line saying the earlier conversation no longer exists and must not be cited, so the model does not reach for something it cannot see

The rebuilt opening has this shape. The lab prints it in Chinese, because both editions of this course share one codebase and the completion marker is a fixed token in that code; the structure is what matters:

TextText
-- What follows is not a replay of the earlier conversation.
   It is a progress summary rebuilt from the state on disk. --
(It was written to disk after every step, so it outlives the previous window.
 The earlier conversation is gone. Do not cite it.)
 
Progress: 3 of 40 done -- F01 F02 F03
Spent so far: 1 window, 3 steps.
 
Rule for what comes next: take the first item on the list above that is not
in the progress line and do it. Do not redo anything already listed there.

Those 175 characters buy this: completed items go from 3 to 9, and wasted steps go from 6 to 0.

Why the state file is structured data rather than free text

A fair objection: if it all gets rendered into one block of text anyway, why not store that text on disk directly?

Because the harness has to read it too.

The model only needs the prose. The harness needs answers to "how many attempts on F23", "is the completed count over budget", "which item did the last crash land on". Against free text those become regular expressions, and a regular expression breaks silently the day someone rewords a line. As JSON they are field accesses.

The division of labor is clean: structured data on disk, rendered to text before it reaches the model. Rendering is a pure function, so rewording changes nothing the harness decides. Today's summarizeState is that renderer, and the only place in the course where on-disk state becomes model context.

Source Reading

Two new modules today, and three places worth slowing down for.

Position one: the atomic write. A plain writeFileSync is "truncate, then write". Die between those two and disk holds half a JSON document. That is worse than no state at all: with no state the next run knows it starts from nothing, while a half file either explodes on parse or, worse, parses a ledger missing half its entries and redoes finished work.

store.js
// Step 1: write into a temp file in the same directory, and fsync it to the platter
function writeStateTemp(stateDir, state) {
  const tmp = join(stateDir, `.run.json.tmp-${process.pid}-${nextSeq()}`)
  const fd = openSync(tmp, 'wx')
  try {
    writeSync(fd, JSON.stringify(state, null, 2) + '\n')
    // Without this line the rename is still atomic, but the bytes may still sit in page cache
    fsyncSync(fd)
  } finally {
    closeSync(fd)
  }
  return tmp
}
 
// Step 2: atomic rename. On one filesystem, any reader sees either the whole
// old state or the whole new state. There is no in-between
function commitStateTemp(stateDir, tmpPath) {
  renameSync(tmpPath, join(stateDir, 'run.json'))
}

Two details matter. The temp file must sit in the same directory as run.json, because rename is only atomic within one filesystem. That mistake is invisible on a laptop where everything is one disk, and only detonates in a container with separate mounts. The temp name also carries a sequence number on top of the process id, because a timestamp alone collides inside the same millisecond and wx mode throws on collision, which becomes a crash that only appears on fast machines.

One capability boundary, stated rather than forgotten: strictly speaking the parent directory needs an fsync after the rename before the metadata is durable. This lab stops at file-level fsync plus atomic rename. Past that the subject is filesystem semantics rather than harness design, and platforms diverge sharply. This is a drawn boundary, not an oversight.

Position two: reading it back without swallowing errors. A few lines, and the easiest thing to get wrong today.

load-state.js
// Correct: a missing file returns null, because that is a brand new run, not an error.
// A file that exists but does not parse into a complete state throws
function loadState(stateDir) {
  const file = join(stateDir, 'run.json')
  if (!existsSync(file)) return null
  const parsed = JSON.parse(readFileSync(file, 'utf8')) // let a broken file throw
  if (!isRunState(parsed)) throw new Error(`corrupt state file: ${file}`)
  return parsed
}
 
// Wrong: this silently turns one data corruption into one run from scratch.
// Unattended, nobody sees it; in the morning progress is simply half of what you expected
function loadStateSwallow(stateDir) {
  try {
    return JSON.parse(readFileSync(join(stateDir, 'run.json'), 'utf8'))
  } catch {
    return null
  }
}

That catch-and-return-null shape is a harmless fallback interactively. Unattended it is a silent failure: no exception, no warning, just a quiet reset of a whole night of progress. Bad state has to be loud.

Position three: where rebuilding attaches. Two lines at the window boundary, with one decision hiding inside them.

rebuild-hook.js
for (let w = 1; w <= config.windows; w += 1) {
  transcript.length = 0 // identical to D1: the context is still wiped
 
  // This reads disk, not the in-memory state object.
  // Progress in memory was alive the whole time (D1 proved that), and building
  // the opening from it would work just as well -- but then the window boundary
  // could only ever be a function call, never a process restart
  const disk = config.withState ? loadState(stateDir) : null
  const opening = config.withState ? rebuildContext(base, disk ?? state, features) : base
  // ...
}

Notice that today's change is not "stop losing the context". That wipe line is identical to yesterday's and the window is still volatile. What changed is what gets spliced on afterwards.

One more thing to watch. model/mock.ts is untouched today, and the self-test pins it with a content hash. The withState switch belongs to the harness and is never passed into AskModel. That is why yesterday's degradation disappears on its own rather than being special-cased away: the model does not know the state layer exists. It simply found progress, for the first time, where it has always looked.

Hands-On Lab

🧪 Day 2: Move the Authoritative State Out of the Window

Code location: labs/agent-harness-7days/day-02-state-layer

Today's lab is the same code run twice: once with the state layer off, once with it on. There are five exercises, three in src/state/store.ts (the two atomic write steps plus the read back) and two in src/state/rebuild.ts (the summary and the splice). Yesterday's four frozen files are copied verbatim; not one character changes.

  1. Read the state layer hook in src/core/loop.ts first, and see exactly where the two new lines attach and why they read disk instead of memory.
  2. Fill in the two atomic write steps and loadState in src/state/store.ts. A parse failure must throw and must never be caught.
  3. Fill in summarizeState and rebuildContext in src/state/rebuild.ts.
  4. Run MOCK=1 pnpm selftest until all 27 self-test assertions are green.
  5. Run MOCK=1 pnpm start and confirm completed items go from 3 to 9 and wasted steps go from 6 to 0.

When it finishes, compare the two ledgers. This is the thing today most worth seeing yourself: the run with the state layer off still leaves a complete ledger in work/no-state/state/run.json, saying done = [F01 F02 F03]. What was lost was never the data. It was the evidence. The ledger was written the whole time; nobody put it into the opening of the next window.

Today's mutation check is not "switch the state layer off", since pnpm start already runs both ways. The one in the README is better: open summarizeState, take the single completion marker the model recognizes, hard-coded as DONE_MARK at the top of src/model/mock.ts, and replace it in the progress line with any other word. Change nothing else. Run again.

TextText
completed items   3 -> 3
wasted steps      6 -> 6

The entire state layer has silently stopped working. The ledger is on disk, the summary really is spliced into the context, and the model still cannot see it, because that marker is the convention the model side hard-codes: it recognizes only that token plus the feature ids on the same line.

Interview Questions

Today's four questions circle the criteria for drawing the state boundary, the goal you set when rebuilding context, and a more fundamental skill: proving the thing you added is what made the difference.

The third is worth extra practice. "I added a state layer and the numbers improved" is a trap. The interviewer is not listening for what you added, but for how you ruled out coincidence. Without a mutation check, everything clever you said before it gets marked down.

Checklist and Tomorrow

  • Can name the three kinds of information that must go into the on-disk state and the two kinds that belong only in the context window, with the test used to decide each
  • Can explain how an equivalent context differs from the original context, and give a concrete case where restoring the original context makes things worse
  • Can bring yesterday's duplicated work back by switching the state layer off, and use that mutation check to prove the state layer is really doing the work
  • Can say why the state file lives outside the target repository, and name the exact moment mixing them fails
  • Can explain what each of the two atomic write steps prevents, and why the temp file must share a directory
  • Got all 27 self-test assertions green with MOCK=1 pnpm selftest, and compared the two ledgers
  • Ran the marker-swap mutation check once, and can describe the shape of that silent failure

Tomorrow is D3, The Initializer Agent: init.sh, a Progress File and the First Commit. Today settled where progress is recorded. An earlier question is still open: when a brand new window opens, how does it know how to get this project running at all? State tells it how far the work got, not which command to run after pnpm install, which port the service listens on, or how the tests are invoked. Rediscovering that every window is the most wasteful of the four failure modes. Tomorrow makes initialization a phase of its own, producing init.sh, a progress note and the first commit, plus a checkable standard: any fresh window is productive inside three minutes.

Interview questions

  • Designing the state layer for a long-horizon agent, what goes to disk and what stays out of it?设计一个长时程 Agent 的状态层,你会把哪些东西写进磁盘,哪些坚决不写?
    Common in ChinaCommon overseasIntermediate#state-management#long-horizon#persistence

    How to reason about it · think before answering

    1. This tests whether you have a criterion, not whether you can recite a list. Plenty of people can rattle off 'progress, logs, context, tool results'. The discriminator is giving a rule someone else could apply to their own project.
    2. Lead with the criterion: what does the next window absolutely need in order to choose the right next action? Then add the sharper inverse test: can this information be recomputed? If yes, do not store it - fetch it fresh when needed. If no, it must be stored, because it is history, and history cannot be replayed.
    3. Filtered through those, only three classes survive. First, what is done - store feature ids, not descriptions, because ids are stable and descriptions drift with requirements. Second, what is in flight, whose value is easy to underrate: if the process dies mid-task, disk holds a dangling 'working on F07'. That is not dirty data; it is precisely what tells the next window that F07 may or may not have landed, so verify before assuming. Drop it and the next window either redoes the work or skips something that was never finished. Third, how many attempts, whose value comes entirely from accumulating across windows - one failed attempt in one window is noise, twelve failures across five windows is a signal that must fire. Loop detection and budget cutoffs are both built on that counter.
    4. Two classes stay out. Intermediate reasoning: once the conclusion has landed, the reasoning has done its job, and keeping it both bloats the file and makes the next window spend attention reading a stale plan. One-shot tool receipts: the output of some git status, the contents of some file read. Their defining trait is that you can fetch them again and the fresh copy is the correct one. Feeding a three-hour-old git status into a new window is worse than feeding nothing - it will act on a stale snapshot.
    5. Raise the placement question too, since many candidates miss it: the state file belongs outside the repository being worked on. Putting it inside means it lands in the agent's own commits, and rolling back to the last good point rolls the ledger back with it. Rollback targets code; it should not also erase the lesson 'I have already tried this three times'. One-line test: deliverables belong to the repo, harness bookkeeping belongs to the state directory.
    6. Expected follow-up: why store structured data when it all gets rendered to text for the model anyway? Because the harness itself reads it - how many attempts on F23, is the completed count over budget, which item did the last crash land on. Against free text those become regexes, and a regex breaks silently when someone rewords one line. As JSON they are field accesses. The split is: structured on disk, rendered to text before the model, with rendering as a pure function.

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

    1. 这题考的是有没有判据,不是能不能列清单。照着「进度、日志、上下文、工具结果」背一串东西的人很多,区分度在于你能不能给出一条**别人可以拿去套自己项目**的判断规则。
    2. 先把判据摆出来:**下一个窗口要做出正确的下一步,非知道不可的是什么?** 再补一条更好用的反向判据:**这条信息能不能重新算出来?** 能重算的不存,用的时候现取;不能重算的必须存,因为那是历史,历史不可重放。
    3. 按这两条筛,必须落盘的只有三类。一是**做完了什么**,存 feature 编号而不是描述文本——编号稳定,描述会随需求改。二是**正在做什么**,它的价值容易被低估:进程崩在半路时磁盘上会留下一条悬空的「正在做 F07」,那不是脏数据,它恰恰告诉下一个窗口「F07 成没成不知道,先确认,别当成做完了」。丢掉它,下一个窗口要么重做一遍,要么直接跳过一条根本没做完的。三是**试了几次**,这类信息的价值完全来自跨窗口累计——单看一个窗口「试了一次没成」是噪声,跨五个窗口「试了十二次都没成」是一个必须响的信号,打转检测和预算熔断全建在这个计数上。
    4. 坚决不写的有两类。**中间推理**:结论落地之后它的使命就结束了,留着既占地方又会让下一个窗口花注意力读一段已作废的思路。**一次性的工具回执**:某次 git status 的输出、某次读文件的内容——它们的特点是随时能重新获取,而且重新获取的那份才是对的。把三小时前的 git status 存下来喂给新窗口,比不给还糟,它会拿着一份过期快照做决定。
    5. 还有一个位置问题值得主动提,很多人答不到:**状态文件放在被操作的仓库之外**。写进去会有两个后果——它会进 agent 自己的 commit,而且回滚到上一个可用点时账本会被一起回滚掉。回滚的目标是代码,不该把「我已经试过三次」这种教训也一起忘掉。一句话判据:交付给用户的属于仓库,harness 自己的簿记属于状态目录。
    6. 可预期的追问是「为什么状态存结构化数据,反正最后都要拼成文本喂模型」。因为 harness 自己要读它:F23 试了几次、已完成条数超预算没有、上次崩在哪条上——对着自由文本只能用正则去抠,而正则会在文案改一个字时悄悄失效。存 JSON,这些都是字段访问。分工是磁盘上存结构化数据,喂模型前渲染成文本,渲染是一个纯函数。

    Key points

    • Test one: what must the next window know? Test two: can this be recomputed?
    • Three classes persist: what is done (ids, not descriptions), what is in flight, how many attempts.
    • The dangling 'in flight' record is useful: it makes the next window verify instead of assume.
    • Attempt counts matter only when accumulated across windows; loop detection and budget cutoffs rest on them.
    • Two classes stay out: intermediate reasoning, and one-shot tool receipts that can be refetched.
    • Keep the state file outside the worked repo, or it lands in the agent's commits and dies on rollback.
    • Structured on disk, rendered to text for the model, because the harness queries it by field.

    答题要点

    • 判据一:下一个窗口非知道不可的是什么。判据二:这条信息能不能重新算出来。
    • 必存三类:做完了什么(存编号不存描述)、正在做什么、试了几次。
    • 「正在做什么」的悬空记录是有用信息:它让下一个窗口去确认而不是假设。
    • 「试了几次」的价值来自跨窗口累计,打转检测与预算熔断都建在它上面。
    • 不存两类:中间推理(结论落地即作废)、一次性工具回执(能重取,且重取的才对)。
    • 状态文件放在被操作的仓库之外,否则会进 agent 的 commit、并被回滚一起抹掉。
    • 磁盘存结构化数据、喂模型前渲染成文本,因为 harness 自己要按字段查询它。
  • After a window reset, is replaying the full prior transcript into the model a good idea?窗口重置后,把之前的完整对话历史重新喂给模型是个好主意吗?
    Common in ChinaCommon overseasIntermediate#context-engineering#long-horizon#cost

    How to reason about it · think before answering

    1. This looks like a cost question, and answering only 'too expensive, will not fit' caps your score. The interviewer wants the second layer: a restored transcript is not merely more expensive, it is harder to act on. Few candidates get there.
    2. Cover cost first for baseline credit. A measured figure you can quote: three steps produce roughly 960 characters of raw transcript, while the rebuilt summary is about 175 - more than fivefold. Over a night that is hundreds of thousands of tokens against hundreds. Restoration is also self-defeating: you switched windows because the context did not fit, so stuffing it back in overflows immediately.
    3. Now the real discriminator. Replay the night's stream and the model has to re-derive, from hundreds of steps, which items are actually done. That inference is lossy, costly and entirely unnecessary, because the harness already holds that conclusion in state. Making the model recompute something the caller knows is a design waste.
    4. Give a concrete case where restoring is worse. In window one the model attempts F07, writes broken code, fails verification, then switches approach and completes F08. Replay that verbatim and the new context contains both the failed F07 code and the later conclusion. The model may well treat the failed code as the current implementation and keep editing it - in context it looks identical to working code. The summary keeps one line: F08 done, F07 attempted once, failed. The ambiguity is gone. Less information, better decisions.
    5. So the answer is to rebuild an equivalent context rather than restore the original: lossy with respect to transcript, lossless with respect to the decision - drop intermediate reasoning and tool receipts, keep everything the next choice depends on.
    6. Expected follow-up: how do you know what you dropped was not decision-critical? Offer an operational check - run the rebuilt opening on its own and see whether the model picks the expected next action. The course lab asserts exactly this: rebuild from disk state alone and assert the model proceeds to F10 rather than looping back to F01.
    7. Another follow-up worth preparing: is this just context compaction? Not quite. Compaction shortens the transcript itself; rebuilding skips the transcript entirely and renders from structured state. One takes history text as input, the other takes fields. They compose, but they are not the same thing.

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

    1. 这题看起来是道成本题,但只答「太贵了、装不下」拿不到高分。面试官想听的是另一层:**还原出来的上下文不只是更贵,它还更难用。** 能说出这一层的人很少。
    2. 先把成本说清楚,它是基础分。实测过一个数字可以直接用:跑三步产生的原始对话约 960 字符,重建出来的摘要约 175 字符,五倍多的差。跑一整夜是几十万 token 对几百 token。而且还原是不可持续的——上下文本来就是因为装不下才换窗口的,把装不下的东西原样塞回去,新窗口会立刻再次撑爆。
    3. 然后是真正的区分点。把一晚上的流水原样喂回去,模型得**自己从几百步里重新推断**出「我到底做完了哪几条」。这是一次有误差、有成本、而且完全没必要的重新推断——这个结论 harness 手上明明已经有了,它就记在状态里。让模型去重算一件调用方已知的事,是设计上的浪费。
    4. 举一个还原反而更糟的具体情形,这是拉开差距的地方:窗口 1 里模型试着实现 F07,写了一段有问题的代码,验证没过,于是改用另一种写法做完了 F08。把这段对话原样还原,新窗口的上下文里就**同时存在**那段失败的 F07 代码和后来的结论。模型很可能把那段失败代码当成现有实现去接着改——在上下文里它和成功的代码长得一模一样。摘要则只留一行「F08 已完成,F07 试过 1 次未成」,歧义消失了。**信息少了,决策反而更准。**
    5. 所以正解是重建一个**等效上下文**而不是还原原上下文:做一次有损压缩,但对决策无损——丢掉中间推理与工具回执(有损),保留下一步决策所需的全部输入(无损)。
    6. 可预期的追问是「那怎么判断压掉的东西是不是决策必需的」。给一条可操作的验收方式:拿重建出来的开场单独跑一遍,看模型选的下一步是否与预期一致。本课 lab 里有一条断言就是这么写的——只拿磁盘状态重建上下文,断言模型接着做的是 F10 而不是回头做 F01。
    7. 还有一个追问值得准备:「那不就是上下文压缩吗」。不完全是。压缩是把对话本身变短,重建是**根本不用对话**,直接从结构化状态渲染。前者的输入是历史文本,后者的输入是字段。两者可以叠加,但不是一回事。

    Key points

    • No. On cost: three steps of raw transcript measure about 960 characters versus about 175 rebuilt.
    • Restoration is self-defeating - you switched windows because it did not fit, so it overflows again.
    • The bigger issue is usability: the model re-derives a conclusion the harness already holds.
    • Concrete case: failed code and the later conclusion coexist, and the model may keep editing the failed code.
    • Rebuild an equivalent context - lossy on transcript, lossless on the next decision.
    • Acceptance check: run the rebuilt opening alone and assert the model picks the expected next action.
    • Rebuilding is not compaction: one takes history text as input, the other takes structured fields.

    答题要点

    • 不是好主意。成本上:实测三步的原始对话约 960 字符,重建摘要约 175 字符。
    • 还原不可持续:上下文本来就是装不下才换窗口的,塞回去会立刻再次撑爆。
    • 更关键的是还原更难用:模型要自己从几百步里重新推断出 harness 已知的结论。
    • 具体情形:失败的旧代码与后来的结论并存,模型可能把失败代码当现有实现接着改。
    • 正解是重建等效上下文——对对话有损,对下一步决策无损。
    • 验收方式:拿重建出的开场单独跑,断言模型选的下一步符合预期。
    • 重建不等于压缩:压缩的输入是历史文本,重建的输入是结构化字段。
  • You added a state layer and the metrics improved. How do you show the state layer caused it, rather than coincidence?你加了一个状态层,跑下来指标变好了。怎么证明是它起的作用,而不是碰巧?
    Common in ChinaCommon overseasDeep dive#evaluation#mutation-testing#state-management

    How to reason about it · think before answering

    1. This is the daily work of the role and the most commonly underrated question. Answering 'I ran an A/B and the enabled arm did better' earns baseline credit only - it shows the two arms differ, not that the difference came from your change.
    2. Layer one is the precondition for a control: apart from the thing under test, everything must match - same code, same model, same script, same target, only the switch differs. Raise the trap proactively: the switch must never reach the model side. Once the model can sense whether the feature is on, it can play along - work properly when enabled, act forgetful when not. The curve looks great but measures cooperation, not effect. The course's model interface takes a single string precisely to hold this line.
    3. Layer two is the mutation check, and this is the real discriminator: do not only observe that enabling it improved things - turn it off and confirm the old behavior returns unchanged. With only the first half, a change that never took effect could still coincide with better numbers. Both halves close the causal loop.
    4. Layer three separates strong answers: mutate the smallest, most load-bearing point rather than disabling the whole layer. Disabling everything is easy but proves less. The lab example is instructive - leave the state layer entirely intact and change only the agreed marker word in the summary from 'done' to a synonym. Completed items drop from nine back to three and wasted steps go from zero back to six. That pins the causal chain to 'the model read progress evidence it recognizes in context', not to 'we wrote an extra file'.
    5. Layer four is writing assertions in both directions, a tautology trap that is easy to fall into. Asserting only 'the duplication disappeared' is insufficient - an implementation that does nothing at all also satisfies 'no duplicates'. Assert the other half too: the first window contains no repeats and did complete its three items. Both of this course's first-draft assertions were tautologies, caught only by mutation testing.
    6. Close with the risk specific to this kind of change: state-layer failure is silent. No exception, no warning, the state file looks normal, code review sees nothing - you just find half the expected progress in the morning. So acceptance cannot rest on 'the metric looks good'; you need an assertion that pins the agreed format itself.
    7. Expected follow-up: what if a real project cannot give you this clean a control? Answer in two parts. Pin down what you can - same task set, same model version, fixed seeds. For what you cannot - real model nondeterminism - repeat and report a distribution, and state where your confidence comes from instead of concluding from a single run.

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

    1. 这题是本岗位每天的功课,也是最容易被轻视的一道。答「跑了 A/B 对比,开的那组更好」只能拿基础分——那只证明了两组有差,没证明差来自你改的那个东西。
    2. 第一层要说的是**对照的前提**:除了被测的那一处,两次运行的其他一切必须相同。同一份代码、同一个模型、同一份剧本、同一个靶子,只有开关不同。这里有个必须主动提的坑:**开关绝不能传进模型侧**。一旦模型能感知当前开没开,它就能配合演出——开了好好干、关了装傻,曲线很漂亮,但你测的是配合度不是效果。本课的模型接口只收一个字符串,就是为了守住这条。
    3. 第二层是**变异检验**,这才是真正的区分点:不只看「开了之后变好」,还要**关掉它确认旧现象原样回来**。只有前半句时,一个根本没生效的改动也可能因为别的原因让指标变好。两边都做到,因果链才闭合。
    4. 第三层最能拉开差距:**变异检验要挑那个最小的、最关键的点去改,而不是整层关掉。** 整层关掉容易,但它证明的东西比较弱。本课 lab 里那个例子很典型:状态层其它部分一个字不动,只把摘要里「已完成」这个约定词换成「做好了」——完成条数立刻从 9 掉回 3,白干步数从 0 回到 6。这说明效果确实来自「模型在上下文里读到了它认得的进度证据」这条因果链,而不是来自「多写了一个文件」。
    5. 第四层是**断言必须双向写**,这是个容易踩的恒真陷阱。只断言「重复现象消失了」是不够的——一个什么活都不干的实现也能让「无重复」成立。所以要同时断言反向那一半:第一个窗口内部没有重复,**并且确实做满了三条**。本课两条断言的第一版都是恒真的,靠变异检验才发现。
    6. 最后提一句这类改动特有的风险:状态层的失效是**静默**的。不抛异常、不打警告、状态文件看着完全正常、代码评审也看不出问题,只是早上进度比预期少了一半。所以验收不能只看「指标好不好」,要有一条断言直接钉住那个约定的格式本身。
    7. 可预期的追问是「真实项目里没法做这么干净的对照怎么办」。分两步答:能控制的部分(同一批任务、同一个模型版本、固定随机种子)尽量控死;控不住的部分(真实模型的随机性)用重复多次取分布,并且**明确说出置信度的来源**,而不是拿单次运行下结论。

    Key points

    • An A/B shows the arms differ, not that your change caused it. Baseline credit only.
    • A control requires everything matched but the tested point, and the switch must never reach the model.
    • Mutation check: also turn it off and confirm the old behavior returns, closing the causal loop.
    • Stronger: mutate the smallest load-bearing point - change only the agreed marker and completions fall from nine to three.
    • Write assertions both ways, or 'the symptom disappeared' is a tautology satisfied by doing nothing.
    • State-layer failure is silent, so assert the agreed format itself, not just the metric.
    • When real runs cannot be controlled, repeat for a distribution and state where confidence comes from.

    答题要点

    • A/B 只证明两组有差,不证明差来自你改的那处,这只是基础分。
    • 对照的前提是除被测点外一切相同,而且开关绝不能传进模型侧。
    • 变异检验:不只看开了变好,还要关掉确认旧现象原样回来,因果链才闭合。
    • 更强的做法是改最小的关键点:只换掉约定词,完成数从 9 掉回 3。
    • 断言双向写,否则「现象消失」是恒真的——什么都不做也能成立。
    • 状态层失效是静默的,所以要有断言直接钉住约定格式本身。
    • 真实项目里控不住随机性时,重复取分布并说清置信度来源,不拿单次下结论。
  • What happens if the process is killed while the state file is being written?状态文件在写入过程中进程被杀了怎么办?
    Common in ChinaCommon overseasDeep dive#crash-safety#persistence#state-management

    How to reason about it · think before answering

    1. This tests imagination about failure shapes. 'Wrap it in try/catch' or 'validate after writing' both miss - the question is not whether writing errors, it is what a mid-write death leaves on disk.
    2. Name the defect first. Overwriting in place means truncate-then-write: the file is emptied, then filled. Die between those and disk holds a half-written JSON. That is worse than no state at all - with no state the next run knows to start over, while a half file either explodes at parse time or, worse, parses into a partial progress record and redoes finished work.
    3. The fix is write-temp-then-atomic-rename, in two steps: write the complete content to a temp file in the same directory and fsync it, then rename it onto the real name. Rename within one filesystem is atomic, so a reader at any instant sees either the old complete state or the new complete state, never something in between. Dying between the steps leaves the old complete state plus an unclaimed temp file - garbage, not a trap.
    4. Three details show field experience and are worth volunteering. The temp file must be in the same directory: rename is atomic only within a filesystem, and writing to a temp dir then moving degrades to a copy - an error that cannot reproduce on one local disk and only bites in a container with separate mounts. The fsync is not optional: without it rename stays atomic but the bytes may sit in page cache, so a power loss can leave the name pointing at an empty block. And the temp name needs a counter: timestamps alone collide within a millisecond, and exclusive-create mode throws on collision, producing a crash that only appears on fast machines.
    5. The other half matters equally: do not swallow errors on read. Wrapping the parse in try and returning empty is a harmless fallback interactively; unattended it is a silent failure that converts one corruption into a full restart nobody witnesses. You just see half the progress in the morning with no way to trace why. Bad state must be loud.
    6. Finish by naming your limits, which scores well: strictly, the parent directory needs fsync after rename before the metadata is durable, and the semantics differ across platforms. Stopping at file-level fsync plus atomic rename is a stated trade-off, not an oversight - being able to say where your solution ends shows more judgment than adding another layer.
    7. Expected follow-up: is writing on every step too slow? Measure before arguing: the state file is a few hundred bytes, while each step contains a model call and process startup, so the write disappears into the noise. If it ever mattered, batch consecutive small writes rather than abandoning crash safety - unattended, that trade costs a whole night.

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

    1. 这题考的是**故障形态的想象力**。答「加个 try catch」或者「写完校验一下」都没打到点上——问题不在写的时候会不会报错,在于死在中间会在磁盘上留下什么。
    2. 先说清楚坏在哪。直接覆盖写是「截断后再写」:先把文件清空,再往里写内容。进程死在这两件事之间,磁盘上留下的是一个**半截的 JSON**。这比没有状态更糟——没有状态时下一次运行知道自己要从头开始,有个半截文件时它要么在解析上炸掉,要么更坏:解析出一份缺了一半的进度,然后把已经做完的事再做一遍。
    3. 正解是**先写临时文件再原子改名**,两步:第一步把完整内容写进**同目录**的临时文件并 fsync,第二步 rename 成正式文件。同一文件系统上的 rename 是原子的,所以任何时刻去读,看到的要么是旧的完整状态、要么是新的完整状态,不存在中间态。死在第一步之后第二步之前,磁盘上是「旧的完整状态加一个没人认的临时文件」——那个临时文件是垃圾,不是陷阱。
    4. 三个细节能体现实战经验,值得主动说。**临时文件必须同目录**:rename 只有在同一文件系统上才原子,写到临时目录再搬过来就退化成拷贝了,而这个错在本机测不出来(同一块盘),只有到了容器里挂载不同卷时才炸。**fsync 不能省**:少了它 rename 仍然是原子的,但内容可能还在页缓存里,断电后会出现「文件名指向一个内容为空的块」。**临时文件名要带自增序号**:只用时间戳会在同一毫秒内撞名,而排他创建模式撞名直接抛错,那会变成一个只在快机器上偶发的崩。
    5. 另一半同样重要:**读的时候不许吞错。** 把 JSON 解析包在 try 里、失败返回空,是交互式场景的无害兜底,在无人值守场景里它是一类静默故障——一次数据损坏被悄悄变成一次从头重跑,没人看得见,早上只看到进度少了一半,而且完全查不出为什么。坏状态必须响。
    6. 最后主动交代能力边界,这一步很加分:严格地说 rename 之后还要 fsync 父目录,元数据才算真落盘;各平台语义还不一样。做到文件级 fsync 加原子 rename 是一个明确的取舍,不是忘了——**能说出自己方案的边界在哪,比多做一层更能体现判断力。**
    7. 可预期的追问是「每一步都写盘不会太慢吗」。先量再说:状态文件是几百字节量级,而一步里有模型调用和进程启动,写盘那点开销在噪声里。真要优化也是先合并连续的小写,不是放弃崩溃安全——无人值守场景里这一条的代价是一整夜。

    Key points

    • In-place overwrite is truncate-then-write; dying mid-way leaves a half JSON, worse than no state.
    • The fix is two steps: write a temp file in the same directory with fsync, then rename it into place.
    • Rename within one filesystem is atomic, so readers see old-complete or new-complete, never partial.
    • Same directory (cross-volume degrades to copy and cannot reproduce locally), fsync is required, and add a counter to the temp name.
    • Do not swallow read errors: catching and returning empty turns corruption into a silent full restart.
    • State your limits: parent-directory fsync is omitted deliberately, not forgotten.
    • On performance, measure: a few hundred bytes disappears next to a model call.

    答题要点

    • 直接覆盖写是截断后再写,死在中间会留下半截 JSON,比没有状态更糟。
    • 正解是两步:写同目录临时文件加 fsync,再 rename 成正式文件。
    • 同一文件系统上 rename 原子,读到的要么是旧的完整状态要么是新的,没有中间态。
    • 临时文件同目录(跨卷会退化成拷贝,本机测不出来)、fsync 不能省、文件名带自增序号防撞名。
    • 读的时候不许吞错:catch 掉返回空会把一次损坏静默变成一次从头重跑。
    • 能力边界要明说:父目录 fsync 没做,是取舍不是遗漏。
    • 性能追问先量:状态文件几百字节,开销淹没在模型调用里。

Comments