// Blog articles — full English versions of the Habr originals, hosted here.
// Self-contained on purpose: an article page loads site-data.js + page-shell.jsx
// + this file only, and never pulls the 90 KB of pages-data.jsx it does not use.

const { Fragment: AF } = React;

const A_LEAD = ({ children }) => (
  <div style={{ fontSize: 14, lineHeight: 1.7, color: '#e8e4d4', marginBottom: 18, paddingBottom: 14, borderBottom: '1px dashed #2a2720' }}>
    {children}
  </div>
);

const A_H = ({ children }) => (
  <div style={{ fontFamily: '"Instrument Serif", Times, serif', fontStyle: 'italic', fontSize: 22, color: '#e8e4d4', margin: '22px 0 10px' }}>
    {children}
  </div>
);

const A_P = ({ children }) => (
  <p style={{ fontSize: 13, lineHeight: 1.75, color: '#c4baa3', margin: '0 0 12px' }}>{children}</p>
);

const A_UL = ({ items }) => (
  <ul style={{ margin: '0 0 12px', paddingLeft: 18, color: '#c4baa3', fontSize: 13, lineHeight: 1.7 }}>
    {items.map((it, i) => <li key={i} style={{ marginBottom: 5 }}>{it}</li>)}
  </ul>
);

const A_CODE = ({ children }) => (
  <pre style={{
    background: '#0a0907', border: '1px solid #2a2720', color: '#6b9b5a',
    padding: '10px 12px', fontSize: 11, lineHeight: 1.55, overflowX: 'auto',
    margin: '0 0 12px', fontFamily: '"JetBrains Mono", monospace',
  }}>{children}</pre>
);

// A pulled-out fact or verdict. Used sparingly — it stops meaning anything if
// every third paragraph is in a box.
const A_NOTE = ({ children }) => (
  <div style={{
    borderLeft: '3px solid #ff5b1f', background: 'rgba(255,91,31,0.06)',
    padding: '10px 13px', margin: '0 0 14px', fontSize: 13, lineHeight: 1.7, color: '#e8e4d4',
  }}>{children}</div>
);

const A_TABLE = ({ head, rows }) => (
  <div style={{ overflowX: 'auto', marginBottom: 14 }}>
    <table style={{ borderCollapse: 'collapse', fontSize: 11.5, color: '#c4baa3', width: '100%', minWidth: 420 }}>
      <thead>
        <tr>{head.map((h, i) => (
          <th key={i} style={{ textAlign: 'left', padding: '6px 10px 6px 0', borderBottom: '1px solid #3a362d', color: '#6b6558', fontWeight: 400, letterSpacing: '.08em', textTransform: 'uppercase', fontSize: 9.5 }}>{h}</th>
        ))}</tr>
      </thead>
      <tbody>
        {rows.map((r, i) => (
          <tr key={i}>{r.map((c, j) => (
            <td key={j} style={{ padding: '6px 10px 6px 0', borderBottom: '1px dotted #2a2720', verticalAlign: 'top' }}>{c}</td>
          ))}</tr>
        ))}
      </tbody>
    </table>
  </div>
);

const ALL_ARTICLES = [
  { slug: '45mb-of-sessions',    title: '45 MB of Claude Code sessions you never look at', date: '2026-04-30' },
  { slug: 'diffusion-seams-40mp', title: 'Why diffusion draws seams on 40-megapixel photos', date: '2026-04-26' },
  { slug: 'many-claudes-one-repo', title: 'Several Claudes on one repo: locks, handoffs, and email from 1982', date: '2026-04-23' },
  { slug: '785-articles-for-agents', title: '785 articles. 26 domains. For agents, not humans.', date: '2026-04-22' },
  { slug: '582-line-claude-md',  title: 'My CLAUDE.md is 582 lines. Here is why.', date: '2026-04-12' },
];

function makeArticle(meta, body) {
  const related = ALL_ARTICLES.filter(a => a.slug !== meta.slug).slice(0, 4);
  return {
    pageLabel: 'blog',
    active: 'blog',
    crumb: 'blog/' + meta.slug,
    terminalBody: (
      <AF>
        <div style={{ color: '#ff5b1f', fontSize: 9.5, letterSpacing: '.15em', textTransform: 'uppercase', marginBottom: 8 }}>
          ✦ blog · {meta.date}
        </div>
        <div style={{ fontSize: 30, fontWeight: 700, lineHeight: 1.18, color: '#e8e4d4', marginBottom: 6, fontFamily: '"JetBrains Mono", monospace' }}>
          <span style={{ color: '#ff5b1f' }}>&gt;_ </span>{meta.title}
        </div>
        <div style={{ color: '#6b6558', fontSize: 11, marginBottom: 18 }}>
          {meta.reach}
          {meta.habr && (
            <AF> · <a href={meta.habr} target="_blank" rel="noopener" style={{ color: '#6b9b5a' }}>Russian original on Habr ↗</a></AF>
          )}
        </div>
        {body}
        <div style={{ marginTop: 22, paddingTop: 14, borderTop: '1px dashed #2a2720', fontSize: 12 }}>
          <a href="/blog/" style={{ color: '#6b9b5a', textDecoration: 'none' }}>← all posts</a>
        </div>
      </AF>
    ),
    windows: [
      {
        title: 'post.meta', accent: '#0000aa',
        body: (
          <div style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 10, lineHeight: 1.75 }}>
            <div>date · <b>{meta.date}</b></div>
            <div>reach · <b>{meta.reach}</b></div>
            <div>tags · {meta.tags.join(' · ')}</div>
            {meta.habr && (
              <div style={{ marginTop: 6, paddingTop: 5, borderTop: '1px dashed #aaa' }}>
                <a href={meta.habr} target="_blank" rel="noopener" style={{ color: '#0000aa' }}>Russian original ↗</a>
              </div>
            )}
          </div>
        ),
      },
      {
        title: 'more-posts', accent: '#008000',
        body: (
          <div style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 10, lineHeight: 1.6 }}>
            {related.map(a => (
              <div key={a.slug} style={{ marginBottom: 6 }}>
                <a href={'/blog/' + a.slug + '/'} style={{ color: '#0000aa' }}>{a.title}</a>
                <div style={{ color: '#888', fontSize: 9 }}>{a.date}</div>
              </div>
            ))}
            <div style={{ marginTop: 6, paddingTop: 4, borderTop: '1px dashed #aaa' }}>
              <a href="/blog/" style={{ color: '#0000aa' }}>all posts →</a>
            </div>
          </div>
        ),
      },
    ],
  };
}

// ═══════════════════════════════════════════════════════════════════
// 1 · My CLAUDE.md is 582 lines
// ═══════════════════════════════════════════════════════════════════
const ART_CLAUDEMD = makeArticle(
  { slug: '582-line-claude-md', date: '2026-04-12', title: 'My CLAUDE.md is 582 lines. Here is why.',
    reach: '44K reads · Habr', tags: ['agents', 'claude-code', 'systems'],
    habr: 'https://habr.com/ru/articles/1022578/' },
  <AF>
    <A_LEAD>
      Every new chat with a coding agent starts from zero. It does not know your project, does not
      remember what you discussed an hour ago in the next window, has no idea that a particular port on
      that server must not be touched. You explain the same thing for the fifth time, and on the sixth
      it still goes and "fixes" a config that was working fine.
    </A_LEAD>

    <A_P>
      Meanwhile r/ClaudeAI produces a fresh horror story every week. An agent dropped a production
      database. An agent pushed secrets to a public repo. An agent "optimised" a billing service and
      invoiced every customer zero. Each time you think: I would rather not become the person in that
      headline.
    </A_P>
    <A_P>
      A CLAUDE.md is supposed to solve both problems — context between sessions, and protection from
      catastrophe. The typical five-to-ten-line CLAUDE.md solves neither. I decided to treat it as an
      architecture problem rather than a list of wishes. It is now 582 lines and six layers, and every
      rule in it exists because something specific happened.
    </A_P>

    <A_H>Three incidents that changed everything</A_H>
    <A_P>
      <b style={{ color: '#e8e4d4' }}>The agent "fixed" a working system.</b> Sunday evening. It sees
      <code style={{ color: '#6b9b5a' }}> 127.0.0.1</code> in a config for an external storage service and
      concludes that a previous session left a mistake — surely that should be the real address. It
      substitutes the real IP. Uploads break. Half an hour of debugging later: it was an SNI proxy through
      a local tunnel, and 127.0.0.1 was the correct value. Without context, the obvious fix was the
      catastrophe.
    </A_P>
    <A_P>
      Rule that came out of it: <i>do not change configs without understanding why the current values are
      what they are. If a value looks strange, understand it first, act second.</i>
    </A_P>
    <A_P>
      <b style={{ color: '#e8e4d4' }}>fail2ban took the agent for a brute-forcer.</b> It was checking
      server state and opened a new SSH connection per check. A dozen connections in a minute read as an
      attack, and the IP was banned for half an hour — while a training run was going on that box, which
      I then could not reach.
    </A_P>
    <A_P>
      Rule: <i>one SSH bridge for everything, one client per session, and never separate check / fix /
      verify scripts — batch them into one call.</i>
    </A_P>
    <A_P>
      <b style={{ color: '#e8e4d4' }}>"Filter" turned out to mean "delete".</b> I asked it to filter a
      dataset — remove unsuitable images. It read that literally and deleted the files. Not moved, not
      flagged. Deleted.
    </A_P>
    <A_P>
      Rule: <i>"filtering" means move or mark, never delete. Before any deletion, confirm the user asked
      for deletion in those words.</i>
    </A_P>
    <A_NOTE>Writing "be careful" does not work. You need a system.</A_NOTE>

    <A_H>Six layers</A_H>
    <A_P>None of the layers was planned. Each appeared after a specific failure.</A_P>
    <A_UL items={[
      <AF><b style={{ color: '#e8e4d4' }}>Rules (9 files).</b> Loaded per situation. Writing an article does not need the SSH rules; debugging does not need the prose rules.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Memory (78 files).</b> Appeared when the agent forgot a server's configuration for the third time. Infrastructure facts, project decisions, my preferences, past mistakes. Files link to each other as <code>[[filename]]</code> — 178 cross-links, a knowledge graph made of plain markdown.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Handoffs.</b> Appeared when a new chat walked straight back into the previous chat's dead end. At the end of a session the agent writes down: what was done, what did NOT work (the valuable part), and the one next action.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Chronicles.</b> Appeared when, after twenty handoffs, it was no longer clear how the project got to its current state. A handoff answers "what next"; a chronicle answers "how did we get here".</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Hooks.</b> Appeared when the rule "validate the links in CLAUDE.md" stopped being obeyed twenty minutes into a session. See below.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Skills (16).</b> Packaged knowledge for specific jobs. The description is written as a trigger for the model — "use when: GPU hung, server health check" — not as a description for humans.</AF>,
    ]} />

    <A_H>A rule is a wish. A hook is a guarantee.</A_H>
    <A_P>
      This is the least obvious thing I learned in a month. A rule in CLAUDE.md is an instruction in a
      prompt: the model can forget it, reinterpret it, or drop it late in a long session when the context
      is full of other things. My "check the links before working" rule held for about ten minutes.
    </A_P>
    <A_P>
      A hook is a Python script the harness runs on an event — SessionStart, Stop, PreToolUse. It does not
      forget and does not reinterpret. It executes, every time.
    </A_P>
    <A_CODE>{`# remind_handoff.py  (Stop hook, simplified)
age = session_age_minutes()
if age < 15:
    return                      # short session, nothing to hand off

if fresh_handoff_exists():
    return                      # already written

print(json.dumps({              # block the close, ask for the handoff
    "decision": "block",
    "reason": f"Session ran {int(age)} min with no handoff. "
              f"Write one into .claude/handoffs/ before exiting."
}))`}</A_CODE>
    <A_NOTE>If something must happen every time, it is a hook, not a rule.</A_NOTE>

    <A_H>One config line that stopped a supply-chain attack</A_H>
    <A_P>
      On 31 March 2026 a DPRK-linked group compromised the official npm package <code>axios</code>
      (~100M downloads a week) and published a malicious 1.14.1. The window was about three hours.
    </A_P>
    <A_P>My <code>.npmrc</code> had one line in it:</A_P>
    <A_CODE>min-release-age=7</A_CODE>
    <A_P>
      Packages published less than seven days ago do not install. Most malicious packages are caught
      within one to three days; seven is a comfortable buffer. It cost nothing and I was not affected.
      The Python equivalent, in <code>uv.toml</code>:
    </A_P>
    <A_CODE>exclude-newer = "7 days"</A_CODE>

    <A_H>Behind the config: 37 papers</A_H>
    <A_P>
      A good share of the rules did not come from my own scar tissue but from academic work — 37 arXiv
      papers reworked into principles. The four that changed my workflow most:
    </A_P>
    <A_UL items={[
      <AF><b style={{ color: '#e8e4d4' }}>Proof loop.</b> The agent says "tests pass"; you check, and they do not. The proof loop forbids an agent from signing off its own work: you need artefacts — test output, a verdict from a verifier in a fresh session that never saw the work being done.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Structured reasoning.</b> Instead of free-form "well, maybe this or maybe that": what we know for certain from code and logs → step-by-step trace → what follows → which hypotheses were tested and dropped.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Deterministic orchestration.</b> Anything deterministic — tests, linters, formatting — goes through a shell script. Models lose count in loops and confuse branch conditions. Scripts do not.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Red lines.</b> Ordinary rules get creatively interpreted. Red lines are absolute prohibitions with no exceptions, each tied to an incident.</AF>,
    ]} />

    <A_H>Numbers</A_H>
    <A_P>
      78 memory files. 178 cross-links. 27 handoffs in four days. 96.9% KV-cache hit rate across 83
      sessions in a week. The config maintains itself: after every change the agent checks whether links
      went stale, and a SessionStart hook validates that automatically.
    </A_P>
    <A_P>
      Does it work perfectly? No. An audit found four memory files that had fallen out of the index —
      documentation drift, inside the system built to prevent documentation drift. It would have been
      worse without it.
    </A_P>

    <A_H>What I do not know</A_H>
    <A_P>
      I am not convinced everyone needs all of it. For most projects five would probably do:
      deterministic orchestration, structured reasoning, supply-chain defence, codified context, handoffs.
      I am not convinced six layers is the minimum either — I may have over-built. But in a month no
      context was lost and no dead end repeated.
    </A_P>
    <A_P>
      One of the principles says it directly: every harness component encodes an assumption about what the
      model cannot do on its own, and models improve. Remove components and measure. Some layer here is
      probably already unnecessary.
    </A_P>
    <A_P>
      Everything is MIT:{' '}
      <a href="https://github.com/AnastasiyaW/codex-claude-code-config" target="_blank" rel="noopener" style={{ color: '#6b9b5a' }}>
        github.com/AnastasiyaW/codex-claude-code-config
      </a>{' '}(the repo has since grown into a Claude Code <i>and</i> Codex configuration system, hence the name).
    </A_P>
  </AF>
);

// ═══════════════════════════════════════════════════════════════════
// 2 · 785 articles for agents
// ═══════════════════════════════════════════════════════════════════
const ART_785 = makeArticle(
  { slug: '785-articles-for-agents', date: '2026-04-22', title: '785 articles. 26 domains. For agents, not humans.',
    reach: '1.7K reads · Habr', tags: ['systems', 'agents', 'knowledge'],
    habr: 'https://habr.com/ru/articles/1026666/' },
  <AF>
    <A_LEAD>
      When an agent starts working in an unfamiliar project, it spends the first 30–40% of its tokens
      understanding what is going on around it. Not working — orienting. A README explains motivation, a
      tutorial holds your hand, an API reference lists parameters. None of them answers the question the
      agent actually arrived with: <i>here is my task — which pattern do I copy, and where are the
      landmines?</i>
    </A_LEAD>

    <A_P>
      The second problem is deeper and takes longer to notice. A model knows "everything", but that
      everything is unevenly distributed in the training data. The longer a technology has existed on the
      internet, the more blog posts and Stack Overflow answers it has, and the heavier its weight in the
      model's head. Ask for a LoRA training recipe and the first thing you get is a recipe from five years
      ago — not because it is better, but because there is more of it. For stable technologies that is
      fine. For anything moving fast — model training, agent tooling, security — it means the agent
      quietly drags you into the past.
    </A_P>
    <A_P>
      The obvious cure is to run deep research every time. I do. But deep research is expensive, the limits
      run out immediately, and — most annoying — every research run evaporates with the session. Tomorrow I
      will run the same search on the same topic and spend the same tokens on what I already found out.
    </A_P>
    <A_NOTE>
      Two ideas fall out of that. Research should <b>accumulate</b> rather than evaporate. And the format
      should show the agent what is current <i>now</i>, so that opening a project drops it straight into
      "this is how it is done here".
    </A_NOTE>

    <A_H>The format: a card, not an article</A_H>
    <A_P>
      Every "article" has the same skeleton. Topic name and a one-sentence definition, so the agent knows
      within a second whether it landed in the right place. A Key Facts block: short statements with
      numbers — versions, thresholds, dependencies. A Patterns block with copy-pasteable code carrying a
      language tag. And at the bottom, the gotchas: what breaks, and what fixes it.
    </A_P>
    <A_P>
      No "let us take a look" introductions, no "in this article we examined" conclusions. Minimum
      overhead, maximum signal. The Kafka broker-architecture card is a ZooKeeper-versus-KRaft table, a
      commented config, JMX metrics with alert thresholds, and a step-by-step migration. It does not read
      smoothly. But an agent greps "KRaft migration" and gets a working three-step recipe.
    </A_P>
    <A_P>
      The last block is the most valuable. One line — <i>"min.insync.replicas only takes effect with
      acks=all"</i> — saves thirty minutes of debugging. The agent read it, did not make the mistake, and
      did not burn a retry cycle fixing it.
    </A_P>

    <A_H>Knowledge does not age at one speed</A_H>
    <A_P>
      Algorithms and mathematics are stable: an amortised-analysis card written three years ago is still
      correct. Diffusion training, inference optimisation and ComfyUI nodes change every few months —
      what worked in autumn has moved by spring.
    </A_P>
    <A_P>
      So every domain carries its own freshness threshold. Stable domains have none. In fast domains, a
      card older than 60 days is flagged automatically and a daily hook drops it into a "check and update"
      list. Not a delete alert — a reminder that this topic has probably shifted. Stale cards come to
      revision by themselves instead of quietly misleading the agent.
    </A_P>

    <A_H>A link graph instead of vector search</A_H>
    <A_P>
      The obvious architecture here is a vector database with RAG on top. It finds semantically close
      things fast and works well in production. I deliberately did not use one, for three reasons.
    </A_P>
    <A_P>
      <b style={{ color: '#e8e4d4' }}>A vector store is not human-readable.</b> Markdown I can open in a
      text editor, grep, diff in git, paste into an email, print out. That works today and in ten years,
      on any machine, with no special tooling. A vector store is a binary only its own client understands:
      change vendor, lose the service, break the API, and the data is a pile of numbers.
    </A_P>
    <A_P>
      <b style={{ color: '#e8e4d4' }}>Vectorisation is close to irreversible.</b> An embedding is lossy
      one-way compression. Without the original chunks stored alongside, the embedding file is useless —
      embedding inversion research works on very short fragments, with the same embedder, and needs a
      trained decoder. It also welds you to one embedding model: swap it and you re-index everything,
      because old vectors mean nothing in the new space.
    </A_P>
    <A_P>
      <b style={{ color: '#e8e4d4' }}>And the important one: a link graph encodes adjacency, not
      similarity.</b> Vector similarity finds topics that overlap conceptually. In real work the valuable
      links run between topics that are semantically far apart. My Kafka replication card links to a memory
      profiling card — not because the topics are alike, but because tuning a consumer surfaced a leak, and
      experience made them neighbours. In embedding space "replication in a distributed queue" and "memory
      profiling in Python" are nowhere near each other; semantic search will never build that bridge. A
      hand-written wiki-link does.
    </A_P>
    <A_P>
      Mechanically it is trivial. Cards reference each other as <code>[[flux-klein-9b-inference]]</code>,{' '}
      <code>[[two-phase-commit]]</code>. At build time a hook resolves each slug to a relative path. Slug
      not found → warning in the log, and in strict mode the build fails, so a broken link never reaches
      production. There are 2,100+ of those links. That is the knowledge graph: no embeddings, no vector
      database, no RAG. Markdown and grep.
    </A_P>

    <A_H>The site is just a reflection of the repository</A_H>
    <A_P>
      Everything on happyin.space is a static site built from markdown in an open repository. No database,
      no backend, no hidden layer. <code>docs/kafka/broker-architecture.md</code> becomes{' '}
      <code>/kafka/broker-architecture/</code> after a build. That is the whole mechanism.
    </A_P>
    <A_P>
      Which means it forks in five minutes: clone, install, serve, and you have a local copy with all the
      cards, full search and the link graph. Point your agent at the local build instead of the public one
      — no network round-trip, no deploy wait. A full rebuild takes about five seconds, on an old laptop as
      fast as on a workstation, because underneath is a static site generator the industry has been
      hardening for fifteen years. Not reinventing the wheel; taking a mature tool to a new problem.
    </A_P>

    <A_H>How research gets in</A_H>
    <A_P>
      Several accounts, several chats each, deep research running somewhere most of the time. Every one of
      those chats has the same standing rule: when a research run finishes, drop a copy of the result into
      one central place — a project bound to the knowledge-space repository, into{' '}
      <code>research/inbox/</code>. No manual transfer, no copy-paste.
    </A_P>
    <A_P>
      A second pass then works the inbox: read each raw dump, extract the concrete facts and gotchas,
      repack into the card format, check it against what already exists in that domain so duplicates
      enrich rather than multiply, verify the wiki-links, run the build, read the diff, push. One such
      session usually yields 20–40 new or enriched cards.
    </A_P>
    <A_P>
      Model tiering falls out naturally here, and this is where I finally found a job that fits the
      mid-tier model: "read a raw research dump, extract facts, repack to the card format, compare with
      the neighbours" is mechanical work with language in it. Deciding whether two cards should merge, or
      resolving a conflict between two sources, is understand-decide-design work — that goes to the
      expensive model. The difference is a multiple in cost with no loss of quality.
    </A_P>

    <A_H>Use it, and add to it</A_H>
    <A_P>
      It is MIT. The simplest way to use it is to point your agent at{' '}
      <a href="https://happyin.space/llms.txt" target="_blank" rel="noopener" style={{ color: '#6b9b5a' }}>happyin.space/llms.txt</a>{' '}
      and let it pull cards as needed — that alone cuts the orientation third of the token budget. The next
      level is contributing: the repository carries a CONTRIBUTING.md for people and an AGENTS.md for your
      agent, describing the card format, the density requirements, what belongs in Gotchas and how to open
      a PR. Hand your agent a fresh research result and that guide, and it assembles the card itself.
    </A_P>
    <A_P>
      The more research is in the base, the less of it anyone has to redo. And the more people send cards,
      the more unexpected bridges appear between domains — which are the valuable ones.
    </A_P>
  </AF>
);

// ═══════════════════════════════════════════════════════════════════
// 3 · Several Claudes on one repo
// ═══════════════════════════════════════════════════════════════════
const ART_MANY = makeArticle(
  { slug: 'many-claudes-one-repo', date: '2026-04-23', title: 'Several Claudes on one repo: locks, handoffs, and email from 1982',
    reach: '1.3K reads · Habr', tags: ['mclaude', 'agents', 'systems'],
    habr: 'https://habr.com/ru/articles/1027064/' },
  <AF>
    <A_LEAD>
      I run Claude Code in parallel across three subscriptions. Colleagues run their own, often in the same
      files, often on the same day. Three problems appeared in order, and each of them turned out to be a
      distributed-systems pattern that is forty years old.
    </A_LEAD>

    <A_H>Layer 1: handoffs — "I am done, pick it up"</A_H>
    <A_P>
      First attempt: write everything into a single <code>HANDOFF.md</code> at the end of a session. That
      breaks the moment there is more than one chat — last writer wins, everyone else is overwritten.
    </A_P>
    <A_P>So: append-only. Every session writes its own file with a unique name.</A_P>
    <A_CODE>{`.claude/handoffs/
├── 2026-04-09_14-32_373d1618_drift-validator.md
├── 2026-04-09_16-47_b858f500_dashboard-refactor.md
├── 2026-04-10_11-20_a1c2d3e4_auth-fix.md
└── INDEX.md   (append-only log)`}</A_CODE>
    <A_P>
      The filename encodes date, time, the first eight characters of the session id, and a task slug. Two
      chats physically cannot produce the same name — even in the same second, the session ids differ. The
      race condition is impossible by construction: no locks, no broker, nothing to configure.
    </A_P>
    <A_P>
      INDEX.md is append-only too: a new line at the bottom, never an edit above. Need to move a status
      from ACTIVE to CLOSED? Append a new record. That is exactly how a write-ahead log works in a
      database, and for the same reason: if a process dies mid-write, the log stays consistent.
    </A_P>
    <A_P>
      A handoff is not a diary, it is a contract with the next session. Five sections, one question each:
      the goal, what was done (with paths, so nothing is rebuilt), <b style={{ color: '#e8e4d4' }}>what did
      NOT work</b> and why, current state (working / broken / blocked), and one next action.
    </A_P>
    <A_NOTE>
      The "did not work" section is the one that earns its keep. Twenty-seven handoffs in four days, and no
      dead end was walked into twice — every new session saw the warning. A handoff fits in ~1,500 tokens
      against ~100K of raw session context: roughly 67× compression, losing nothing that matters for
      continuing.
    </A_NOTE>
    <A_P>
      Writing them is automated by a Stop hook that notices a substantive session closing with nothing
      recorded. Reading them is one sentence at the start of the next session. On long projects handoffs
      pile up, so old ones get folded: one rollup summarises twenty, marks the boundary date, and stamps
      each subordinate file with <code>rolled_up_into:</code>. A new session reads one rollup plus whatever
      is newer than its boundary.
    </A_P>

    <A_H>Layer 2: locks — "this resource is mine right now"</A_H>
    <A_P>
      Handoffs only ever append, so nobody overwrites anybody. Real resources are different: two sessions
      cannot refactor one module at once (merge conflict), cannot deploy to staging at once (the second
      overwrites the first), cannot train on one GPU at once (the second gets an OOM).
    </A_P>
    <A_P>
      One file per resource, named after it: <code>auth-middleware.lock</code>,{' '}
      <code>staging-deploy.lock</code>, <code>gpu-host-a_gpu2.lock</code>. A new session lists the folder
      and sees what is taken. The trick is in how the file is created: <code>os.open(path, O_CREAT |
      O_EXCL)</code> is atomic — if the file exists, the call fails. Two processes physically cannot create
      it at once; the kernel guarantees that. It is the same primitive behind <code>/var/lock</code> and
      pid files since the eighties.
    </A_P>
    <A_P>
      A lock without a heartbeat only survives until the first crash: session dies, lock stays, everyone
      waits forever. So an active session refreshes a timestamp every 30–60 minutes, and a lock older than
      four hours is <i>possibly</i> dead. Possibly is not certainly — before taking it over I check the
      resource itself: ssh and ps, nvidia-smi, the deploy queue, git status. Verify externally, then
      seize. Chubby and ZooKeeper do the same thing for the same reason.
    </A_P>
    <A_P>
      My first instinct was a PreToolUse hook that parses commands and marks resources busy automatically.
      Dead end: commands are too varied, and the regex parser becomes a false-positive machine. The right
      order was the opposite — convention and files first, automation later, and only for patterns that
      actually repeat.
    </A_P>

    <A_H>Layer 3: mail — "Artem, look at my code"</A_H>
    <A_P>
      Handoffs pass context through time; locks arbitrate resources. Neither solves synchronisation between
      two <i>live</i> sessions. I set a colleague's agent onto auth-middleware, went to write a plugin in my
      own chat, and an hour later wanted to know where it got stuck. The old answer was: open a messenger,
      ask the colleague, colleague looks at their chat, copies text back. Three human relays, every evening.
    </A_P>
    <A_P>I thought about what I actually needed, and realised I was describing email.</A_P>
    <A_CODE>{`.claude/mailbox/
├── ani/
│   ├── inbox/
│   ├── sent/
│   └── archive/
├── artem/
│   └── ...
└── all/          (broadcast)`}</A_CODE>
    <A_P>
      Each message is a markdown file with YAML frontmatter: from, to, type, subject, message_id,
      in_reply_to, status. My session writes a question to Artem; an hour later he opens his Claude Code, a
      UserPromptSubmit hook checks his inbox before the first prompt, sees the unread file and puts it in
      context. He answers; the reply lands in my inbox. Threading through <code>in_reply_to</code>,
      delivery receipts as separate files, broadcast via <code>to: *</code>, de-duplication through a small
      watcher-state file. No polling, no broker, no background daemon.
    </A_P>
    <A_TABLE
      head={['Email (RFC 822, 1982)', 'Agent mailbox']}
      rows={[
        ['To / Cc', 'addressing a specific session'],
        ['From / Reply-To', 'return address'],
        ['Subject', 'triage without reading the body'],
        ['In-Reply-To', 'message threads'],
        ['Read / Unread', 'seen it or not'],
        ['Inbox per user', 'per-recipient queue'],
        ['Sent folder', 'sender-side audit trail'],
        ['Delivery receipt', 'confirmation without "did you see it?"'],
      ]}
    />
    <A_P>
      Email solves exactly this abstract problem: asynchronous delivery with guarantees between processes
      that may never be online at the same time. Parallel agent sessions are the same problem in new
      packaging.
    </A_P>

    <A_H>The gap in the ecosystem</A_H>
    <A_P>
      Isolation has plenty of solutions: git worktrees, sandboxes, a port per agent, task queues with
      claims. They all solve "sessions do not disturb each other" by making sure there is no shared state.
      Coordination <i>of</i> shared state is nearly empty. The community feature request for a session lock
      file is open and unimplemented; there is a separate issue where a shared config file gets corrupted
      by concurrent writes — one user reported 315 broken backups in seven days.
    </A_P>
    <A_NOTE>
      The ecosystem is mature at avoiding shared state and empty at coordinating it.
    </A_NOTE>

    <A_H>Where it breaks, and what I do not know</A_H>
    <A_UL items={[
      <AF><b style={{ color: '#e8e4d4' }}>Scale.</b> Tested on 3–6 parallel sessions, 2–3 people, one C++ project. What happens at 20+ sessions I have not measured; I suspect de-duplication becomes the bottleneck first.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Across machines without a hub</b> the file core syncs through the shared repository, so latency is minutes rather than seconds.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Lock races on network filesystems.</b> O_CREAT|O_EXCL is atomic locally; on NFS or SMB that guarantee depends on the implementation.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Stale-lock takeover is semi-automatic</b> — the external check is still a human reading metadata and deciding.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Human in the loop is still required.</b> If six agents decide auth-middleware needs rewriting they can negotiate by mail, but who actually takes it is a person's call.</AF>,
    ]} />
    <A_P>
      The three layers are packaged as{' '}
      <a href="https://github.com/AnastasiyaW/mclaude" target="_blank" rel="noopener" style={{ color: '#6b9b5a' }}>mclaude</a>{' '}
      — Python, no dependencies in the core, 193 tests, MIT. Every generation of distributed systems
      rediscovers that email was right. In the 2010s it was message queues. In 2026 it is AI agents.
    </A_P>
  </AF>
);

// ═══════════════════════════════════════════════════════════════════
// 4 · Diffusion seams on 40 MP photos
// ═══════════════════════════════════════════════════════════════════
const ART_SEAMS = makeArticle(
  { slug: 'diffusion-seams-40mp', date: '2026-04-26', title: 'Why diffusion draws seams on 40-megapixel photos',
    reach: '789 reads · Habr', tags: ['ml', 'systems'],
    habr: 'https://habr.com/ru/articles/1028252/' },
  <AF>
    <A_LEAD>
      I spent twenty years as a retoucher and have spent four trying to make diffusion models behave on
      professional frames of 40+ megapixels. Every approach ends up revolving around tiles. And every
      approach has the same two problems: the borders between tiles, and a model that has no idea what it
      is looking at.
    </A_LEAD>

    <A_P>
      Retouching does not live at 2048 pixels a side. Professional cameras still produce enormous files —
      forty megapixels, sixty, a hundred, sometimes a hundred and fifty — and all of it still needs skin,
      hair, colour, light and background work. The standard answer is to cut the image into tiles, process
      each, and glue them back.
    </A_P>
    <A_NOTE>
      The model receives a piece of <i>something</i>. What exactly? Nobody tells it. That is the real
      problem — not the glue.
    </A_NOTE>
    <A_P>
      What I want is tiles that run seamlessly, and a model that knows what it is holding: this is skin,
      here is hair, this is sky, this is metal with a reflection. Not "process this 512×512 patch" but
      "process this part of a portrait; to the left is the cheek, above it the hair".
    </A_P>

    <A_H>How many tiles are we actually talking about</A_H>
    <A_P>
      Tile size sits at 1024–2048, the native resolution of current models. Professional files run 8K–15K
      on the long side: Phase One IQ4 at 14200×10600, Hasselblad H6D-100c at 11600×8700, Canon R5 at
      8192×5464. At tile 1024 with modest overlap that is roughly 75–80 tiles for 60 MP, ~130 for 100 MP,
      ~190 for 150 MP. Push overlap to 25% and the 150 MP file is ~270 tiles; at 50%, about 540.
    </A_P>
    <A_P>
      And blending — the usual way seams are hidden — behaves differently across frequencies. On smooth
      content (sky gradients, skin tone, a soft falloff on metal) a colour transition can be masked well.
      On high frequency (hair texture, stone, grain, fine scratches) you either get a visible step at the
      border or you smear the texture by averaging neighbours. One knob cannot fix both axes: the harder
      you blend, the softer the colour seam and the worse the texture.
    </A_P>
    <A_P>
      Any mechanism whose cost is quadratic in the number of tiles is therefore dead on arrival — it will
      not run in 80 GB of VRAM, and it will not run in 192 GB either.
    </A_P>

    <A_H>Idea one: video models remember frames. Our photo model does not.</A_H>
    <A_P>
      Video diffusion solves an adjacent problem. When a video model generates frame eighteen it does not
      only see frame eighteen: it remembers seventeen, sixteen, some early key frames, sometimes the first.
      That memory lives in the architecture — self-attention over the time axis, accumulated state, a KV
      cache between chunks — and it is why the background does not jitter and the colour does not drift
      between frames.
    </A_P>
    <A_P>
      Our photo model, processing tile (3, 5), has none of that. It gets its pixels and a text prompt. No
      neighbour to the left, no neighbour above, no notion that this is one picture.
    </A_P>
    <A_P>
      Port that memory from the time axis to the spatial grid and you would fix both problems at once:
      seams (tiles remember their neighbours' colour) and part of the semantics (the hair tile knows a neck
      starts below it, not fur). Going through the video literature, there are eight architectural classes
      of memory, and only some of them survive the move to a plane:
    </A_P>
    <A_TABLE
      head={['type', 'mechanism', 'verdict for tiles']}
      rows={[
        ['A · full 3D attention', 'every token sees every token', 'drop — quadratic; will not start at 64+ tiles'],
        ['B · block-causal linear', 'S = ΣKᵀV, Z = ΣKᵀ; constant state', 'primary — ~380 MB of state regardless of tile count'],
        ['C · sliding window', 'attend to the last W frames', 'backup — good locally, blind to distant tiles'],
        ['D · factored space+time', 'temporal attention after each spatial block', 'direct map — "across all frames" becomes "across all tiles"'],
        ['E · anchor frame', 'everything attends to frame 0', 'augment — an anchor tile aligns colour'],
        ['F · geometric compression', 'near = full, far = compressed; total converges', 'primary — neighbours full, diagonals 2×, distant 4×'],
        ['G · cache sharing', 'KV computed once, reused across steps', 'augment — compute neighbour features once'],
        ['H · discrete tokens', 'FSQ vocabulary, next-token', 'drop — incompatible with continuous pixel editing'],
      ]}
    />
    <A_P>
      The three that matter are B, D and F. B keeps two accumulators per layer, so a hundred tiles cost
      what ten cost. F packs near neighbours densely and distant ones compressed, so total context
      converges instead of growing. D is the most literal port: reshape after the spatial block so that
      each pixel position attends across all tiles instead of across all frames.
    </A_P>

    <A_H>What I have actually run</A_H>
    <A_P>
      Only on SANA so far — it was trained at 512 → 1024 → 2K → 4K, so a 1024 tile is native, and it
      already has linear attention in the backbone, which is type B sitting there for free.
    </A_P>
    <A_P>
      I trained a small run on a test dataset of strip sequences at tile 1024, asking two questions: does
      the loss converge, and does the mechanism transfer from the time axis to the spatial plane at all.
      The loss converges, and the transfer works — the model learns to reconcile neighbouring tiles the way
      it used to reconcile neighbouring frames. I am not publishing numbers from a test run.
    </A_P>

    <A_H>Idea two: give the model the whole picture, and the tile's position</A_H>
    <A_P>
      If you have a 6000×4000 photo and cut it into 512×512 tiles, you are holding the whole image. Not
      using it is strange. Downscale it in latent space and feed it alongside every tile — channel concat
      into the first conv layer. Plus the tile's position: two extra channels, (x, y) ∈ [-1, 1], so tile
      (3, 5) knows it is (3, 5) in the grid rather than "some 512 pixels in the void".
    </A_P>
    <A_NOTE>
      PaDIS (NeurIPS 2024) did exactly the coordinate half and measured it: same model, same data, same
      hyperparameters, coordinates on versus off. Without them PSNR fell from 33.57 dB to 23.25 dB. That is
      not the difference between good and acceptable — the authors describe the outputs as very low quality
      across the board.
    </A_NOTE>
    <A_P>
      What goes wrong without position is that the model has to restore every patch with one shared
      function and no way to anchor knowledge to place — "faces usually here, sky gradient there". It
      averages, and averaging looks like noise. Two numbers per pixel costs almost nothing, and almost
      nobody feeds them.
    </A_P>
    <A_P>There are three orthogonal axes of "what the tile should know":</A_P>
    <A_TABLE
      head={['level', 'meaning', 'who does it']}
      rows={[
        ['geometric', '"I am tile (3, 5) of an 8×8 grid"', 'PaDIS coordinate channels'],
        ['visual', '"the whole picture looks like this"', 'DemoFusion, FreeScale, DC-VSR, ResMaster'],
        ['semantic', '"I am processing skin; next to it is hair — do not invent an ear"', 'AccDiffusion v2, per-tile prompt via a VLM'],
      ]}
    />
    <A_P>
      Out of the box, tiled VAE gives you level zero: pixels and one global prompt. For the visual level two
      things work — a downscaled VAE latent of the full image into the first conv layer (keeps spatial
      structure, colour, lighting), or a compact 64-dimensional vector of global properties modulating
      AdaLN-Zero. One thing does not work: a CLIP image embedding through IP-Adapter.
    </A_P>
    <A_P>
      I measured that one rather than assuming it. Counterfactual probe — real preview versus random-noise
      preview — gave a mean difference of 0.0066 per pixel, well under the 0.02 threshold at which I would
      call a signal decorative. The adapter technically runs (120 cross-attention calls per tile) and
      changes essentially nothing; the likely cause is encoder mismatch between what IP-Adapter was trained
      on and the SDXL pair in use.
    </A_P>
    <A_P>
      One implementation note that is not optional: zero-init the first conv projection. Otherwise at step
      zero the model sees its normal input plus random weights and the pretrained behaviour breaks on the
      first forward pass. Zero-init means the architecture starts identical to the backbone and learns to
      use the new channels gradually.
    </A_P>

    <A_H>Bonus idea: fewer tiles rather than smarter ones</A_H>
    <A_P>
      A hypothesis I have not tested. The first half of the denoising steps handles low-frequency structure
      — colour distribution, composition, large elements. That can be done without tiles at all, on a
      downscaled version, in one pass. The second half handles high-frequency detail, and Detail Daemon
      adds that through the sigma schedule rather than a recursive re-denoise. If it holds, half the steps
      run cheaply at small size with no LF seams, and tiling survives only in the HF phase — probably still
      needed above ~2048 a side, since attention maps at 4096 do not fit in 80 GB either.
    </A_P>

    <A_H>What none of this proves</A_H>
    <A_P>
      Negative results are more honest than positive ones, so: the full ablation is not done. I confirmed
      convergence and basic transferability on one SANA run — that is not a full training and not a
      comparison against baselines. I do not know which of the three architectures wins on seam PSNR. I do
      not know how any of it scales to 16×16 grids; every probe ran on 3×3 to 7×7. The semantic level is
      untested. The Detail Daemon idea is an architectural sketch with zero runs behind it.
    </A_P>
  </AF>
);

// ═══════════════════════════════════════════════════════════════════
// 5 · 45 MB of sessions
// ═══════════════════════════════════════════════════════════════════
const ART_45MB = makeArticle(
  { slug: '45mb-of-sessions', date: '2026-04-30', title: '45 MB of Claude Code sessions you never look at',
    reach: '3.7K reads · Habr', tags: ['agents', 'claude-code', 'systems'],
    habr: 'https://habr.com/ru/articles/1030216/' },
  <AF>
    <A_LEAD>
      I run several Claude accounts — not out of luxury, but because weekly limits exist and when one runs
      out I switch. Every switch rebuilds the session list in the sidebar: new face, new projects, and the
      session where I spent two days on colour correction is gone. The files are on disk somewhere. In the
      app window they do not exist.
    </A_LEAD>

    <A_P>
      What I found was more interesting than expected. On this machine there were <b style={{ color: '#e8e4d4' }}>715
      sessions across six accountId folders, about 48 MB of history</b>. The app window showed 69 of them.
      Ten percent. The rest were on disk, readable by cat, grep and Python, and completely invisible
      through the interface.
    </A_P>

    <A_H>Where they physically live</A_H>
    <A_P>
      On macOS: <code>~/Library/Application Support/Claude/claude-code-sessions/&lt;accountId&gt;/&lt;orgId&gt;/local_*.json</code>.
      The folder name is not documented anywhere — it comes out of the app's own bundled JavaScript, where a
      constant holds the string. On Windows the same structure sits under{' '}
      <code>%APPDATA%\Claude\claude-code-sessions\</code>.
    </A_P>
    <A_P>A quick script over the folders gave this:</A_P>
    <A_CODE>{`[372ac280…] sessions=4    size=2K     last=2026-04-13  → a test account
[4d1f33a2…] sessions=330  size=44M    last=2026-04-08  → the main archive (abandoned)
[a4ea2866…] sessions=83   size=4.5M   last=2026-04-10  → archive
[a9e945b6…] sessions=83   size=252K   last=2026-04-26  → recent parallel
[be2110e2…] sessions=69   size=203K   last=2026-04-29  → ACTIVE RIGHT NOW
[c53acc1e…] sessions=141  size=244K   last=2026-04-26  → recent parallel`}</A_CODE>
    <A_P>
      Six accounts, not the four I was sure about. And the one I had most thoroughly forgotten held the
      main archive: 44 MB, 330 sessions, five months of work.
    </A_P>

    <A_H>The toolkit</A_H>
    <A_P>
      Four cross-platform Python scripts, in the public config repo under a{' '}
      <code>desktop-sessions-discovery</code> skill:
    </A_P>
    <A_UL items={[
      <AF><b style={{ color: '#e8e4d4' }}>sessions_registry.py</b> — reads every session from every account and generates one self-contained HTML dashboard with search, sorting and a Restore button per card. The active account is highlighted; anything already migrated is marked from a migration journal.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>sessions_inventory.py</b> — a text report grouped by account, with a summary of projects that appear in more than one.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>sessions_find.py</b> — substring search over titles and working directories, with account and time-window filters.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>sessions_restore.py</b> — migrates one session into the active account: byte-for-byte verification of the copy, a journal entry, and the original is never deleted.</AF>,
    ]} />
    <A_P>
      In practice I say "show me all sessions on this machine for migration", the skill triggers, a browser
      tab opens with the dashboard, I find the 392-step session from the abandoned archive, press Restore,
      paste the command back into the chat, restart the app, and it is in the window.
    </A_P>

    <A_H>What I deliberately did not do</A_H>
    <A_UL items={[
      <AF><b style={{ color: '#e8e4d4' }}>No bulk merge of all 715 sessions.</b> A sidebar with 700 entries — fifty of them empty scheduled runs — is unreadable, and a bulk copy is exactly what a future validity check would reject.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>No automatic migration on startup.</b> Anything nailed to the current storage format turns into a pumpkin the moment the format moves.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>No symlinks between account folders.</b> They break on the first path change and breed strange bugs six months later. Copy only.</AF>,
      <AF><b style={{ color: '#e8e4d4' }}>Originals are never deleted</b> after a restore. They stay in their account folder as the backup.</AF>,
    ]} />

    <A_H>The tool has an expiry date, and that is the point</A_H>
    <A_P>
      The storage format has changed three times in a year — one folder name replaced another, and part of
      the macOS population is already migrating to a disk-image bundle with <code>rootfs.img</code> and{' '}
      <code>sessiondata.img</code> instead of plain JSON. Once that ships, copying <code>local_*.json</code>{' '}
      files will most likely stop working entirely. Separately, Store installs are broken in their own way:
      the atomic rename of a temp file fails inside the packaging sandbox because source and target look
      like different devices, so sessions are not saved at all.
    </A_P>
    <A_NOTE>
      Which is why my actual long-term strategy is not the migration tool. It is the handoff: a markdown
      file the agent writes into <i>my</i> repository at the end of a session — goal, what was done, what did
      not work, current state, one next step. It does not care how the vendor stores chat history. Move the
      sessions to SQLite, to an encrypted disk image, to the cloud with DRM; my handoff is still sitting in
      my project folder in plain markdown, and any new session that opens in that folder reads it.
    </A_NOTE>
    <A_P>
      The two things solve different problems. Restoration reaches into someone else's storage and depends
      on it. A handoff writes state into mine, at the moment I still have the context in my head. The old
      session can be lost completely and the new one still knows the goal, the work, the failures and the
      next step.
    </A_P>
    <A_P>
      One more thing the layout decides for you: the desktop app splits sessions per account, so a handoff
      written next to a session is invisible to a session in another account. The CLI keeps everything in
      one project directory regardless of account — switch accounts and only the credentials file changes.
      That is the quiet reason my work keeps drifting to the CLI.
    </A_P>
  </AF>
);

window.ARTICLES = {
  CLAUDEMD: ART_CLAUDEMD,
  ARTICLES785: ART_785,
  MANYCLAUDES: ART_MANY,
  SEAMS: ART_SEAMS,
  SESSIONS45MB: ART_45MB,
};
