From 9c65291973e95a93729b4c977e64f6a5392f26f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 19:14:14 -0400 Subject: [PATCH] Keep document bodies out of the cacheable system prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildDocumentsSection was inlining up to 5 document bodies at 2000 chars each into the system prompt. Any edit to any of the listed documents between wakes invalidated the Anthropic prompt cache — the prefix that buildTaskPrompt spent real effort to keep stable. Emit metadata only (id, type, title, author, status, version, updatedAt) and teach agents to GET the single-document endpoint when they actually need the body. The single-doc route did not exist, so add it; update the API doc block in the system prompt accordingly. --- src/main/api-server/routes-files.ts | 18 ++++++++++++++ src/main/prompt-builder/api-docs/files.en.md | 6 +++++ src/main/prompt-builder/context-injection.ts | 25 +++++++++----------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/main/api-server/routes-files.ts b/src/main/api-server/routes-files.ts index 5421eb70..a1b2fd7e 100644 --- a/src/main/api-server/routes-files.ts +++ b/src/main/api-server/routes-files.ts @@ -118,6 +118,24 @@ export function registerFileRoutes(server: AgentApiServer) { server.json(res, 200, docs); }); + server.addRoute("GET", "/api/companies/:companyId/documents/:documentId", (_req, res, params, token) => { + if (params.companyId !== token.companyId) { + server.json(res, 403, { error: "Company mismatch" }); + return; + } + const documentId = params.documentId; + if (!documentId) { + server.json(res, 400, { error: "Missing documentId" }); + return; + } + const doc = db.getDocumentForUpdate(documentId, token.companyId); + if (!doc) { + server.json(res, 404, { error: "Document not found in this company" }); + return; + } + server.json(res, 200, doc); + }); + server.addRoute("PATCH", "/api/companies/:companyId/documents/:documentId", (_req, res, params, token, body) => { if (params.companyId !== token.companyId) { server.json(res, 403, { error: "Company mismatch" }); diff --git a/src/main/prompt-builder/api-docs/files.en.md b/src/main/prompt-builder/api-docs/files.en.md index d609880f..672fbe0d 100644 --- a/src/main/prompt-builder/api-docs/files.en.md +++ b/src/main/prompt-builder/api-docs/files.en.md @@ -18,6 +18,12 @@ Body: { ### List Company Documents GET /api/companies/{{companyId}}/documents +### Fetch a Single Document (by id) +GET /api/companies/{{companyId}}/documents/{documentId} +Returns the full document record including `content`. The system prompt +only lists document metadata to keep the cacheable prefix stable — call +this endpoint when you need the actual text. + ### Update Document (with conflict detection) PATCH /api/companies/{{companyId}}/documents/{documentId} Body: { diff --git a/src/main/prompt-builder/context-injection.ts b/src/main/prompt-builder/context-injection.ts index 03127ca5..9a60a9be 100644 --- a/src/main/prompt-builder/context-injection.ts +++ b/src/main/prompt-builder/context-injection.ts @@ -146,23 +146,20 @@ interface DocumentsSectionCtx { export function buildDocumentsSection(ctx: DocumentsSectionCtx): string | null { if (ctx.recentDocuments.length === 0) return null; - const docLines = ctx.recentDocuments.slice(0, 5).map((d) => { + // Intentionally emit metadata only. Inlining document content (even + // truncated to 2000 chars × 5 documents) invalidates the Anthropic + // prompt cache on every wake where any listed document is edited — + // the exact opposite of what buildTaskPrompt's cache-aware layout + // promises. Agents fetch the body with + // GET /api/companies/{companyId}/documents/{id} + // when they actually need it. + const docLines = ctx.recentDocuments.slice(0, 10).map((d) => { const author = d.authorAgentId ? (ctx.allAgents.find(a => a.id === d.authorAgentId)?.name ?? "Unknown") : "System"; - const header = `- [${d.type}] ${d.title} — by ${author}, status: ${d.status}, updated: ${d.updatedAt}`; - if (d.content && d.content.length > 0) { - const contentLimit = 2000; - const excerpt = d.content.length > contentLimit - ? d.content.slice(0, contentLimit) + "..." - : d.content; - return `${header}\n Content:\n ${excerpt.split("\n").join("\n ")}`; - } - return header; + return `- [${d.id}] ${d.type}: "${d.title}" — by ${author}, status: ${d.status}, v${d.version}, updated ${d.updatedAt}`; }); - return `# Recent Documents -${docLines.join("\n\n")} - -Reference these documents when relevant to your current work. Create new documents for important work artifacts. + return `# Recent Documents (metadata only — fetch content via GET /api/companies/{companyId}/documents/{id}) +${docLines.join("\n")} `; }