bullets) {
+ sb.append("## ").append(heading).append('\n');
+ if (bullets == null || bullets.isEmpty()) {
+ sb.append("(none)\n\n");
+ return;
+ }
+ for (String bullet : bullets) {
+ sb.append("- ").append(bullet).append('\n');
+ }
+ sb.append('\n');
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateReason.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateReason.java
new file mode 100644
index 00000000..00de575d
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateReason.java
@@ -0,0 +1,23 @@
+package vip.mate.wiki.hotcache;
+
+/**
+ * Why a hot cache row was last rebuilt. Stored as
+ * {@link Enum#name()} on {@code mate_wiki_hot_cache.update_reason}.
+ */
+public enum HotCacheUpdateReason {
+
+ /** A wiki compile job for some raw material in this KB just finished. */
+ COMPILE_DONE,
+
+ /** A page in this KB was edited directly (not via compile). */
+ PAGE_UPDATED,
+
+ /** A conversation that read pages from this KB ended (debounced). */
+ CONVERSATION_END,
+
+ /** Operator hit the "rebuild now" button. */
+ MANUAL,
+
+ /** Periodic refresh fired because no event had triggered a rebuild for a while. */
+ STALE_CHECK,
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheService.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheService.java
new file mode 100644
index 00000000..89e0fc53
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheService.java
@@ -0,0 +1,49 @@
+package vip.mate.wiki.hotcache;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import vip.mate.wiki.model.WikiHotCacheEntity;
+import vip.mate.wiki.repository.WikiHotCacheMapper;
+
+import java.util.Optional;
+
+/**
+ * Read/write service for KB-level hot cache snapshots.
+ *
+ * This PR provides read methods only — the LLM-driven rebuilder lands
+ * in a follow-up PR (along with the event listener and debounce window).
+ * Read methods are exposed now so the agent-prompt provider can wire up
+ * against a stable API even before any cache rows exist.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WikiHotCacheService {
+
+ private final WikiHotCacheMapper mapper;
+
+ /**
+ * Returns the active hot cache row for {@code kbId}, or empty when
+ * none has ever been generated for this KB. The mapper logical-delete
+ * filter excludes soft-deleted rows automatically.
+ */
+ public Optional findByKb(Long kbId) {
+ if (kbId == null) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(mapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(WikiHotCacheEntity::getKbId, kbId)));
+ }
+
+ /**
+ * Returns the rendered markdown body for {@code kbId}, or {@code null}
+ * when no cache row exists. Convenience for the agent-prompt provider
+ * which only ever needs the body.
+ */
+ public String getContentOrNull(Long kbId) {
+ return findByKb(kbId).map(WikiHotCacheEntity::getContent).orElse(null);
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/model/WikiHotCacheEntity.java b/mateclaw-server/src/main/java/vip/mate/wiki/model/WikiHotCacheEntity.java
new file mode 100644
index 00000000..d3ff6b3a
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/wiki/model/WikiHotCacheEntity.java
@@ -0,0 +1,51 @@
+package vip.mate.wiki.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * One row per knowledge base — the rolling "what happened recently here"
+ * snapshot rendered into the agent system prompt at conversation start.
+ *
+ * The body is markdown produced from four sections (Last Updated, Key
+ * Recent Facts, Recent Changes, Active Threads) by an LLM rebuild call.
+ * This entity is the persistence shape; the value-object form lives in
+ * {@link vip.mate.wiki.hotcache.HotCacheContent}.
+ */
+@Data
+@TableName("mate_wiki_hot_cache")
+public class WikiHotCacheEntity {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long kbId;
+
+ /** Markdown body, target ≤ 4096 chars after rendering. */
+ private String content;
+
+ /** SHA-256 of {@link #content}; lets the updater short-circuit identical rewrites. */
+ private String contentHash;
+
+ private LocalDateTime lastUpdated;
+
+ /** {@link vip.mate.wiki.hotcache.HotCacheUpdateReason#name()} for the most recent rebuild. */
+ private String updateReason;
+
+ private Long rebuildCount;
+
+ private LocalDateTime lastRebuildStartedAt;
+ private Long lastRebuildDurationMs;
+ private String lastRebuildError;
+
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/repository/WikiHotCacheMapper.java b/mateclaw-server/src/main/java/vip/mate/wiki/repository/WikiHotCacheMapper.java
new file mode 100644
index 00000000..276ae0d9
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/wiki/repository/WikiHotCacheMapper.java
@@ -0,0 +1,9 @@
+package vip.mate.wiki.repository;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import vip.mate.wiki.model.WikiHotCacheEntity;
+
+@Mapper
+public interface WikiHotCacheMapper extends BaseMapper {
+}
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V82__wiki_hot_cache.sql b/mateclaw-server/src/main/resources/db/migration/h2/V82__wiki_hot_cache.sql
new file mode 100644
index 00000000..e85ce2cc
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V82__wiki_hot_cache.sql
@@ -0,0 +1,37 @@
+-- mate_wiki_hot_cache: KB-level rolling snapshot of "what happened recently"
+-- in this knowledge base, injected into the agent system prompt so the model
+-- doesn't have to wiki_search the obvious every turn.
+--
+-- One row per KB (uk_whc_kb). Body is markdown rendered from a four-section
+-- structure (Last Updated / Key Recent Facts / Recent Changes / Active Threads),
+-- regenerated by an LLM call after compile/page/conversation events; this
+-- migration only provisions storage. Consumers and updater land in later PRs.
+
+CREATE TABLE IF NOT EXISTS mate_wiki_hot_cache (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ kb_id BIGINT NOT NULL UNIQUE,
+
+ -- Markdown body, target ≤ 4096 chars after rendering.
+ content CLOB,
+
+ -- SHA-256 of `content`; lets the updater skip a write when LLM output
+ -- ends up identical to the previous body.
+ content_hash CHAR(64),
+
+ last_updated TIMESTAMP,
+
+ -- HotCacheUpdateReason enum name (COMPILE_DONE / PAGE_UPDATED /
+ -- CONVERSATION_END / MANUAL / STALE_CHECK).
+ update_reason VARCHAR(32),
+
+ rebuild_count BIGINT NOT NULL DEFAULT 0,
+ last_rebuild_started_at TIMESTAMP,
+ last_rebuild_duration_ms BIGINT,
+ last_rebuild_error VARCHAR(512),
+
+ create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ deleted INT NOT NULL DEFAULT 0
+);
+
+CREATE INDEX IF NOT EXISTS idx_whc_kb ON mate_wiki_hot_cache (kb_id, deleted);
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V82__wiki_hot_cache.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V82__wiki_hot_cache.sql
new file mode 100644
index 00000000..6e8776b7
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V82__wiki_hot_cache.sql
@@ -0,0 +1,31 @@
+-- mate_wiki_hot_cache: KB-level rolling snapshot of "what happened recently"
+-- in this knowledge base, injected into the agent system prompt so the model
+-- doesn't have to wiki_search the obvious every turn.
+--
+-- One row per KB (uk_whc_kb). Body is markdown rendered from a four-section
+-- structure (Last Updated / Key Recent Facts / Recent Changes / Active Threads),
+-- regenerated by an LLM call after compile/page/conversation events; this
+-- migration only provisions storage. Consumers and updater land in later PRs.
+
+CREATE TABLE IF NOT EXISTS mate_wiki_hot_cache (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ kb_id BIGINT NOT NULL,
+
+ content TEXT,
+ content_hash CHAR(64),
+
+ last_updated DATETIME(3),
+ update_reason VARCHAR(32),
+
+ rebuild_count BIGINT NOT NULL DEFAULT 0,
+ last_rebuild_started_at DATETIME(3),
+ last_rebuild_duration_ms BIGINT,
+ last_rebuild_error VARCHAR(512),
+
+ create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
+ deleted TINYINT NOT NULL DEFAULT 0,
+
+ UNIQUE KEY uk_whc_kb (kb_id, deleted),
+ KEY idx_whc_kb (kb_id, deleted)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;