NexBoard Build Journal — Mon 04 May – Sun 10 May 2026

What shipped

  • HawkInc memory system — persistent, file-based long-term memory across Claude Code sessions. INDEX.md loads at startup, project topic files load on task match, department indexes load only when explicitly needed. ~400 tokens overhead for typical sessions
  • Synthesis context overflow fixed — weekly memory synthesis was silently failing; routing slip had grown to 544KB; brief files now embedded, raw session logs listed as file paths only; prompt dropped to ~16KB
  • Dispatcher body truncation pattern — 3,000-char hard cap on routing slip bodies documented; file-reference pattern established for large-payload tasks: write content to a file, pass only the read instruction in the body
  • Synthesis dual-write — synthesis tasks now write to both the deliverables path and a permanent archive using explicit dual-path instructions in the prompt, since the dispatcher’s mandatory instructions override any single alternative path
  • Gitea @claude webhook spec — architecture designed for replicating GitHub’s native @claude feature on self-hosted Gitea: webhook receiver → context assembly → agent dispatch → comment-back flow

The interesting problem

The weekly memory synthesis was consistently reporting “only 1 of 7 agents’ memories provided” — but the task was being dispatched and completing without errors.

The routing slip body was 544KB (~136K tokens). The agent was seeing only the CEO section at the top before running out of context. No error, no truncation warning — just a quiet partial synthesis.

The cause: _build_prompt() in synthesise_memories.py was embedding raw session log files directly into the routing slip. Those files are 50–62KB each.

The fix splits files by type based on filename:

brief_files = [f for f in memory_files if "-brief" in f.name]
raw_files   = [f for f in memory_files if "-brief" not in f.name]

for f in brief_files:          # ~300 bytes each — embed in full
    prompt += f.read_text()

for f in raw_files:            # 50-62KB each — reference only
    prompt += f"Read if needed: {f.name}\n"

Brief files (~300B daily summaries) are embedded. Raw session logs are listed by filename — the agent reads them via its filesystem tool if it needs the detail. Prompt dropped from 544KB to ~16KB, all seven agents now synthesised in a single pass.

Decision of the week

Load memory indexes at startup or on demand?

The memory system has three department indexes covering deliverables, vault files, and departmental content — roughly 17,000 tokens combined. Loading all three at every session start would burn that budget on every short task.

The system loads only the top-level INDEX.md (~150 tokens) and the relevant project index (~250 tokens) at startup. Department indexes load only when the task explicitly needs to retrieve deliverables or vault content.

~400 tokens for 90% of sessions. A bug-fix session pays 400 tokens in memory overhead, not 17,000.

What surprised me

The synthesis had been running with a 544KB routing slip since the feature launched. The task always completed, the deliverable was always written — it just covered only the first agent alphabetically and called it a synthesis. The signal was the output quality, not any error state.

I only noticed when I re-read a report and found nothing from the Engineering or Finance agents.

What’s next

Memory synthesis now works — next is scheduling it weekly via cron and validating output quality over a few runs. The Gitea @claude webhook is spec’d; implementation means adding a webhook receiver to NexBoard and wiring it into the existing dispatch pipeline.


Building NexBoard — a self-hosted AI Chief of Staff, built entirely with Claude Code.