diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheEventListener.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheEventListener.java new file mode 100644 index 00000000..bf89692d --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheEventListener.java @@ -0,0 +1,59 @@ +package vip.mate.wiki.hotcache; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import vip.mate.memory.event.ConversationCompletedEvent; +import vip.mate.wiki.model.WikiKnowledgeBaseEntity; +import vip.mate.wiki.service.WikiKnowledgeBaseService; + +import java.util.List; + +/** + * Wires the wiki hot-cache rebuild trigger into the existing event flow. + * + *

Today only {@link ConversationCompletedEvent} is published — when a + * chat turn ends, we schedule a rebuild for the agent's primary KB. + * (Per the existing chat retrieval path, "primary" is the first entry + * from {@link WikiKnowledgeBaseService#listByAgentId}, ordered by recent + * update.) + * + *

The compile-completed and page-updated events that the design + * envisions don't have publishers in the codebase yet; their listeners + * will land in the PRs that introduce those publishers, to avoid + * shipping dead glue. The scheduler exposes {@code rebuildNowBlocking} + * so the future admin API can drive {@code MANUAL} rebuilds without + * going through events. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HotCacheEventListener { + + private final HotCacheUpdateScheduler scheduler; + private final WikiKnowledgeBaseService kbService; + + @EventListener + public void onConversationEnd(ConversationCompletedEvent event) { + Long agentId = event.agentId(); + if (agentId == null) return; + Long kbId = resolvePrimaryKb(agentId); + if (kbId == null) { + log.debug("[HotCache] agent={} has no KB; skip post-conversation rebuild", agentId); + return; + } + scheduler.scheduleRebuild(kbId, HotCacheUpdateReason.CONVERSATION_END); + } + + private Long resolvePrimaryKb(Long agentId) { + try { + List kbs = kbService.listByAgentId(agentId); + if (kbs.isEmpty()) return null; + return kbs.get(0).getId(); + } catch (Exception e) { + log.debug("[HotCache] KB resolution failed for agent={}: {}", agentId, e.getMessage()); + return null; + } + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheProperties.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheProperties.java new file mode 100644 index 00000000..0dccb953 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheProperties.java @@ -0,0 +1,38 @@ +package vip.mate.wiki.hotcache; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +/** + * Tunables for the wiki hot-cache rebuild pipeline. Defaults match the + * "ship cautiously" preset — short rebuild window, conservative LLM + * timeout, modest input slice. + * + *

Override per-environment via {@code mateclaw.wiki.hot-cache.*}. + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "mateclaw.wiki.hot-cache") +public class HotCacheProperties { + + /** Minimum gap between two LLM-driven rebuilds for the same KB. */ + private Duration debounce = Duration.ofMinutes(5); + + /** Time window for "recent" log/page entries fed to the LLM. */ + private Duration recentWindow = Duration.ofHours(24); + + /** Hard cap on rendered hot cache body length (post-LLM truncation point). */ + private int maxChars = 4096; + + /** Cap on number of recent created/updated pages fed to the prompt (each side). */ + private int maxRecentPages = 10; + + /** Cap on chars from the previous hot cache body fed back as context. */ + private int previousContentCap = 1500; + + /** Cap on chars from the KB activity log markdown fed to the prompt. */ + private int logExcerptCap = 2000; +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheRebuildPromptBuilder.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheRebuildPromptBuilder.java new file mode 100644 index 00000000..b214aee9 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheRebuildPromptBuilder.java @@ -0,0 +1,67 @@ +package vip.mate.wiki.hotcache; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import vip.mate.agent.prompt.PromptLoader; +import vip.mate.wiki.model.WikiPageEntity; + +import java.time.Instant; +import java.util.List; + +/** + * Builds the user-facing prompt for one hot-cache rebuild call by + * substituting placeholders in {@code wiki/hot-cache-rebuild-user.txt}. + * + *

System prompt is loaded once and held identical across calls — the + * intent is to keep prompt-cache reuse high once + * {@code wiki.compile.cache.enabled} is wired in a follow-up PR. + */ +@Component +@RequiredArgsConstructor +public class HotCacheRebuildPromptBuilder { + + private final HotCacheProperties props; + + /** + * Returns a single user-message body with the placeholders substituted. + * Inputs are sliced to the configured caps before injection. + */ + public String buildUser(String previousContent, + String logMarkdownExcerpt, + List recentCreates, + List recentUpdates) { + String template = PromptLoader.loadPrompt("wiki/hot-cache-rebuild-user"); + return template + .replace("{iso_timestamp}", Instant.now().toString()) + .replace("{recent_window}", props.getRecentWindow().toString()) + .replace("{previous_content}", abbreviate(blankToNone(previousContent), props.getPreviousContentCap())) + .replace("{log_excerpt}", abbreviate(blankToNone(logMarkdownExcerpt), props.getLogExcerptCap())) + .replace("{recent_creates}", renderPages(recentCreates)) + .replace("{recent_updates}", renderPages(recentUpdates)); + } + + public String buildSystem() { + return PromptLoader.loadPrompt("wiki/hot-cache-rebuild-system"); + } + + private static String renderPages(List pages) { + if (pages == null || pages.isEmpty()) return "(none)"; + StringBuilder sb = new StringBuilder(); + for (WikiPageEntity p : pages) { + String slug = p.getSlug() == null ? "?" : p.getSlug(); + String title = p.getTitle() == null ? slug : p.getTitle(); + sb.append("- [[").append(slug).append("]] ").append(title).append('\n'); + } + return sb.toString().stripTrailing(); + } + + private static String blankToNone(String s) { + return (s == null || s.isBlank()) ? "(none)" : s; + } + + private static String abbreviate(String s, int max) { + if (s == null) return ""; + if (s.length() <= max) return s; + return s.substring(0, max - 1) + "…"; + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateScheduler.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateScheduler.java new file mode 100644 index 00000000..7a3277d2 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateScheduler.java @@ -0,0 +1,93 @@ +package vip.mate.wiki.hotcache; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import vip.mate.wiki.model.WikiHotCacheEntity; + +import java.time.Instant; +import java.time.ZoneId; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Off-loads hot-cache rebuilds onto a virtual-thread executor and applies + * two safety nets: + * + *

+ * + *

Both checks are best-effort — there's no global coordination, so + * two JVMs can each do one rebuild back-to-back. The + * {@code last_rebuild_started_at} timestamp the updater writes is the + * cross-process signal that PR-2's debounce uses; a future PR can lift + * this to a distributed lock if churn shows up in metrics. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HotCacheUpdateScheduler { + + private final HotCacheProperties props; + private final WikiHotCacheService cacheService; + private final WikiHotCacheUpdater updater; + + private final ConcurrentMap locks = new ConcurrentHashMap<>(); + + /** Virtual threads — rebuilds are LLM-bound, not CPU-bound. */ + private static final ExecutorService REBUILD_EXECUTOR = + Executors.newVirtualThreadPerTaskExecutor(); + + /** Schedule a rebuild for {@code kbId}; returns immediately. */ + public void scheduleRebuild(Long kbId, HotCacheUpdateReason reason) { + if (kbId == null) return; + REBUILD_EXECUTOR.execute(() -> attemptRebuild(kbId, reason)); + } + + /** Synchronous variant — used by tests + the (future) admin API. */ + public void rebuildNowBlocking(Long kbId, HotCacheUpdateReason reason) { + if (kbId == null) return; + attemptRebuild(kbId, reason); + } + + private void attemptRebuild(Long kbId, HotCacheUpdateReason reason) { + ReentrantLock lock = locks.computeIfAbsent(kbId, k -> new ReentrantLock()); + if (!lock.tryLock()) { + log.debug("[HotCache][kb={}] rebuild already running; reason={} skipped", kbId, reason); + return; + } + try { + if (withinDebounceWindow(kbId, reason)) return; + updater.rebuild(kbId, reason); + } catch (Exception e) { + log.warn("[HotCache][kb={}] scheduled rebuild crashed; reason={}: {}", + kbId, reason, e.getMessage(), e); + } finally { + lock.unlock(); + } + } + + private boolean withinDebounceWindow(Long kbId, HotCacheUpdateReason reason) { + // MANUAL bypasses debounce — operators triggering by hand expect to + // see their click take effect. + if (reason == HotCacheUpdateReason.MANUAL) return false; + + WikiHotCacheEntity existing = cacheService.findByKb(kbId).orElse(null); + if (existing == null || existing.getLastRebuildStartedAt() == null) return false; + Instant lastStart = existing.getLastRebuildStartedAt() + .atZone(ZoneId.systemDefault()).toInstant(); + if (Instant.now().isBefore(lastStart.plus(props.getDebounce()))) { + log.debug("[HotCache][kb={}] within debounce window; reason={} skipped", kbId, reason); + return true; + } + return false; + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheUpdater.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheUpdater.java new file mode 100644 index 00000000..86328b13 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheUpdater.java @@ -0,0 +1,266 @@ +package vip.mate.wiki.hotcache; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.stereotype.Service; +import vip.mate.agent.AgentGraphBuilder; +import vip.mate.llm.model.ModelConfigEntity; +import vip.mate.llm.service.ModelConfigService; +import vip.mate.system.featureflag.FeatureFlagService; +import vip.mate.wiki.job.WikiJobStep; +import vip.mate.wiki.job.WikiModelRoutingService; +import vip.mate.wiki.metrics.WikiMetrics; +import vip.mate.wiki.model.WikiHotCacheEntity; +import vip.mate.wiki.model.WikiPageEntity; +import vip.mate.wiki.repository.WikiHotCacheMapper; +import vip.mate.wiki.service.WikiPageService; +import vip.mate.wiki.service.WikiScaffoldService; + +import java.security.MessageDigest; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.List; + +/** + * Performs one LLM-driven hot cache rebuild for a single KB. Caller + * (typically {@link HotCacheUpdateScheduler}) handles debounce + per-KB + * locking; this class is otherwise stateless and idempotent. + * + *

Pipeline: gather inputs (previous body + activity log excerpt + + * recent creates / updates) → build a system+user prompt pair → resolve a + * KB-routed chat model the same way the wiki narrative refresher does → + * call → truncate to the configured cap → diff against the existing + * content hash → write only if changed. + * + *

Failure modes (flag off, model resolution miss, LLM error) all + * surface as warnings in the row's {@code last_rebuild_error}. They never + * propagate up — the triggering event must complete regardless of cache + * health. + */ +@Slf4j +@Service +public class WikiHotCacheUpdater { + + private static final String FLAG = "wiki.hot_cache.enabled"; + private static final RetryTemplate NO_RETRY = RetryTemplate.builder().maxAttempts(1).build(); + + private final WikiHotCacheService cacheService; + private final WikiHotCacheMapper mapper; + private final HotCacheRebuildPromptBuilder promptBuilder; + private final HotCacheProperties props; + private final WikiModelRoutingService modelRoutingService; + private final ModelConfigService modelConfigService; + private final AgentGraphBuilder agentGraphBuilder; + private final FeatureFlagService featureFlagService; + private final WikiMetrics metrics; + private final WikiPageService pageService; + + public WikiHotCacheUpdater(WikiHotCacheService cacheService, + WikiHotCacheMapper mapper, + HotCacheRebuildPromptBuilder promptBuilder, + HotCacheProperties props, + WikiModelRoutingService modelRoutingService, + ModelConfigService modelConfigService, + AgentGraphBuilder agentGraphBuilder, + FeatureFlagService featureFlagService, + WikiMetrics metrics, + WikiPageService pageService) { + this.cacheService = cacheService; + this.mapper = mapper; + this.promptBuilder = promptBuilder; + this.props = props; + this.modelRoutingService = modelRoutingService; + this.modelConfigService = modelConfigService; + this.agentGraphBuilder = agentGraphBuilder; + this.featureFlagService = featureFlagService; + this.metrics = metrics; + this.pageService = pageService; + } + + /** + * Rebuild the hot cache row for {@code kbId}. Safe to call concurrently; + * the scheduler holds the lock that makes it serial per KB. + */ + public void rebuild(Long kbId, HotCacheUpdateReason reason) { + if (kbId == null) return; + if (!featureFlagService.isEnabledForKb(FLAG, kbId)) { + log.debug("[HotCache][kb={}] flag disabled; skip rebuild reason={}", kbId, reason); + return; + } + + Instant start = Instant.now(); + markRebuildStarted(kbId); + + try { + // 1. Inputs. + String prevContent = cacheService.getContentOrNull(kbId); + LocalDateTime since = LocalDateTime.now().minus(props.getRecentWindow()); + List recentCreates = + pageService.findRecentCreated(kbId, since, props.getMaxRecentPages()); + List recentUpdates = + pageService.findRecentUpdated(kbId, since, props.getMaxRecentPages()); + String logExcerpt = readLogExcerpt(kbId); + + // 2. Cheap exit: if the KB has had no activity at all in the + // window, skip the LLM call entirely (the snapshot would be + // no different from what's already there). + if (recentCreates.isEmpty() && recentUpdates.isEmpty() + && (logExcerpt == null || logExcerpt.isBlank())) { + log.debug("[HotCache][kb={}] no recent activity; skip rebuild reason={}", kbId, reason); + clearRebuildMarker(kbId); + return; + } + + // 3. Resolve a chat model for this KB. + ChatModel chatModel = resolveChatModel(kbId); + if (chatModel == null) { + log.debug("[HotCache][kb={}] no chat model resolvable; skip rebuild", kbId); + recordError(kbId, "no chat model resolvable", + Duration.between(start, Instant.now()).toMillis()); + return; + } + + // 4. Build prompts + call LLM. + String system = promptBuilder.buildSystem(); + String user = promptBuilder.buildUser(prevContent, logExcerpt, recentCreates, recentUpdates); + Prompt prompt = new Prompt(List.of(new SystemMessage(system), new UserMessage(user))); + String rendered = chatModel.call(prompt).getResult().getOutput().getText(); + + // 5. Truncate + persist. + String truncated = truncate(rendered, props.getMaxChars()); + if (truncated == null || truncated.isBlank()) { + log.warn("[HotCache][kb={}] LLM returned empty body; treating as failure", kbId); + recordError(kbId, "LLM returned empty body", + Duration.between(start, Instant.now()).toMillis()); + return; + } + String hash = sha256(truncated); + persistRebuild(kbId, truncated, hash, reason, start); + + metrics.recordCompileStage("hot-cache-rebuild", kbId, + Duration.between(start, Instant.now())); + log.info("[HotCache][kb={}] rebuilt; reason={} chars={} duration_ms={}", + kbId, reason, truncated.length(), + Duration.between(start, Instant.now()).toMillis()); + + } catch (Exception e) { + log.warn("[HotCache][kb={}] rebuild failed; reason={}: {}", + kbId, reason, e.getMessage(), e); + recordError(kbId, e.getMessage(), + Duration.between(start, Instant.now()).toMillis()); + // Intentionally swallow — caller's event must complete. + } + } + + private void persistRebuild(Long kbId, String content, String hash, + HotCacheUpdateReason reason, Instant start) { + WikiHotCacheEntity row = cacheService.findByKb(kbId).orElseGet(() -> { + WikiHotCacheEntity fresh = new WikiHotCacheEntity(); + fresh.setKbId(kbId); + fresh.setRebuildCount(0L); + return fresh; + }); + + boolean unchanged = hash.equals(row.getContentHash()); + if (!unchanged) { + row.setContent(content); + row.setContentHash(hash); + row.setRebuildCount((row.getRebuildCount() == null ? 0L : row.getRebuildCount()) + 1); + } + row.setLastUpdated(LocalDateTime.now()); + row.setUpdateReason(reason.name()); + row.setLastRebuildDurationMs(Duration.between(start, Instant.now()).toMillis()); + row.setLastRebuildError(null); + + if (row.getId() == null) { + mapper.insert(row); + } else { + mapper.updateById(row); + } + if (unchanged) { + log.debug("[HotCache][kb={}] body unchanged; only timestamps refreshed", kbId); + } + } + + /** + * Reads the most recent slice of the KB's activity log markdown page + * (created by {@code WikiLogService.append}). Bounded to + * {@link HotCacheProperties#getLogExcerptCap()} chars from the tail — + * older sections are dropped. Returns null if the log page is absent. + */ + private String readLogExcerpt(Long kbId) { + try { + WikiPageEntity log = pageService.getBySlug(kbId, WikiScaffoldService.LOG_SLUG); + if (log == null) return null; + String content = log.getContent(); + if (content == null) return null; + int cap = props.getLogExcerptCap(); + if (content.length() <= cap) return content; + // Tail: latest entries are at the bottom of the markdown. + return "…\n" + content.substring(content.length() - cap); + } catch (Exception e) { + log.debug("[HotCache][kb={}] log page read failed: {}", kbId, e.getMessage()); + return null; + } + } + + private ChatModel resolveChatModel(Long kbId) { + try { + Long modelId = modelRoutingService.selectModelId(kbId, "hot_cache_rebuild", WikiJobStep.SUMMARY); + ModelConfigEntity model = modelConfigService.getModel(modelId); + if (model == null) return null; + return agentGraphBuilder.buildRuntimeChatModel(model, NO_RETRY); + } catch (Exception e) { + log.debug("[HotCache][kb={}] model routing failed: {}", kbId, e.getMessage()); + return null; + } + } + + private void markRebuildStarted(Long kbId) { + cacheService.findByKb(kbId).ifPresent(row -> { + row.setLastRebuildStartedAt(LocalDateTime.now()); + mapper.updateById(row); + }); + } + + private void clearRebuildMarker(Long kbId) { + cacheService.findByKb(kbId).ifPresent(row -> { + row.setLastRebuildStartedAt(null); + mapper.updateById(row); + }); + } + + private void recordError(Long kbId, String error, long durationMs) { + cacheService.findByKb(kbId).ifPresent(row -> { + row.setLastRebuildError(error != null && error.length() > 500 + ? error.substring(0, 500) : error); + row.setLastRebuildDurationMs(durationMs); + row.setLastRebuildStartedAt(null); + mapper.updateById(row); + }); + } + + private static String truncate(String s, int max) { + if (s == null) return ""; + if (s.length() <= max) return s; + return s.substring(0, max - 1) + "…"; + } + + private static String sha256(String s) { + try { + byte[] digest = MessageDigest.getInstance("SHA-256") + .digest(s.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(64); + for (byte b : digest) sb.append(String.format("%02x", b)); + return sb.toString(); + } catch (Exception e) { + return "no-hash"; + } + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiPageService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiPageService.java index e4fd9e15..1fa2e91b 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiPageService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiPageService.java @@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import vip.mate.wiki.model.WikiPageEntity; import vip.mate.wiki.repository.WikiPageMapper; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -72,6 +73,47 @@ public class WikiPageService { * default {@link #listByKbId} filter. Used by the admin UI's "show archived" * panel so users can see what they archived and recover it. */ + /** + * Pages in {@code kbId} created at or after {@code since}, newest first. + * Used by the hot-cache rebuilder to surface "what was just added"; + * archived pages and system pages (overview/log) are excluded so the + * snapshot stays focused on user-visible knowledge. + */ + public List findRecentCreated(Long kbId, LocalDateTime since, int limit) { + if (kbId == null || since == null || limit <= 0) return java.util.List.of(); + List rows = pageMapper.selectList( + new LambdaQueryWrapper() + .eq(WikiPageEntity::getKbId, kbId) + .ne(WikiPageEntity::getArchived, 1) + .ne(WikiPageEntity::getPageType, WikiScaffoldService.SYSTEM_PAGE_TYPE) + .ge(WikiPageEntity::getCreateTime, since) + .orderByDesc(WikiPageEntity::getCreateTime) + .last("LIMIT " + limit)); + rows.forEach(p -> p.setContent(null)); + return rows; + } + + /** + * Pages in {@code kbId} updated at or after {@code since}, newest first. + * Same exclusions as {@link #findRecentCreated}. + * + *

A row that was both created and updated in the window will appear + * in both lists — the caller deduplicates if needed. + */ + public List findRecentUpdated(Long kbId, LocalDateTime since, int limit) { + if (kbId == null || since == null || limit <= 0) return java.util.List.of(); + List rows = pageMapper.selectList( + new LambdaQueryWrapper() + .eq(WikiPageEntity::getKbId, kbId) + .ne(WikiPageEntity::getArchived, 1) + .ne(WikiPageEntity::getPageType, WikiScaffoldService.SYSTEM_PAGE_TYPE) + .ge(WikiPageEntity::getUpdateTime, since) + .orderByDesc(WikiPageEntity::getUpdateTime) + .last("LIMIT " + limit)); + rows.forEach(p -> p.setContent(null)); + return rows; + } + public List listArchivedByKbId(Long kbId) { List pages = pageMapper.selectList( new LambdaQueryWrapper() diff --git a/mateclaw-server/src/main/resources/prompts/wiki/hot-cache-rebuild-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/hot-cache-rebuild-system.txt new file mode 100644 index 00000000..39af1874 --- /dev/null +++ b/mateclaw-server/src/main/resources/prompts/wiki/hot-cache-rebuild-system.txt @@ -0,0 +1,39 @@ +You are the wiki hot cache rebuilder. Your job: produce a concise "what +happened recently" snapshot for the given knowledge base, so the next AI +session can pick up where the last one left off without re-reading every +wiki page. + +Output STRICT markdown with this exact structure: + +--- +type: meta +updated: {iso_timestamp} +--- + +## Last Updated +{One sentence summarizing the most recent activity, ≤ 100 chars.} + +## Key Recent Facts +- {Most important takeaway from recent ingest, ≤ 80 chars} +- {Second most important, ≤ 80 chars} +- {Third most important, ≤ 80 chars} + +## Recent Changes +Created: {wikilinks separated by commas, e.g. [[page-1]], [[page-2]]} +Updated: {same shape} +Flagged: {contradictions or ambiguous links worth attention} + +## Active Threads +- {Open research direction, ≤ 100 chars} +- {Open question, ≤ 100 chars} + +CONSTRAINTS: +- TOTAL output (including frontmatter) MUST be ≤ 4000 characters. +- Use ONLY information from the inputs the user provides; do NOT + speculate, do NOT invent page slugs. +- Sections may be empty (write "(none)" if so), but the four headers + MUST appear, in order, exactly as shown. +- Output ONLY the markdown above — no fences, no preamble, no + explanation, no closing remarks. +- Match the language of the input pages (Chinese inputs → Chinese + snapshot). diff --git a/mateclaw-server/src/main/resources/prompts/wiki/hot-cache-rebuild-user.txt b/mateclaw-server/src/main/resources/prompts/wiki/hot-cache-rebuild-user.txt new file mode 100644 index 00000000..c9b42cec --- /dev/null +++ b/mateclaw-server/src/main/resources/prompts/wiki/hot-cache-rebuild-user.txt @@ -0,0 +1,21 @@ +## Previous hot cache (for continuity; may be empty) + +{previous_content} + +## Recent activity log excerpt (last {recent_window}) + +{log_excerpt} + +## Recently created pages + +{recent_creates} + +## Recently updated pages + +{recent_updates} + +--- + +Produce the rebuilt hot cache snapshot now, using the structure and +constraints in the system prompt. Use the timestamp {iso_timestamp} in +the frontmatter.