From 20d50242d9ca622c31d95cb0b0668c0962d048dc Mon Sep 17 00:00:00 2001 From: Bailey Dixon Date: Thu, 16 Jul 2026 10:41:05 -0400 Subject: [PATCH 1/2] fix: initialize Hermes chat instructions before delivery --- DEVLOG.md | 12 +++ src/app/api/chat/stream/route.ts | 92 ++++++++++--------- tests/unit/chat-stream-initialization.test.ts | 18 ++++ 3 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 tests/unit/chat-stream-initialization.test.ts diff --git a/DEVLOG.md b/DEVLOG.md index 388f3057..9a0df2e0 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,16 @@ > Append-only session log. Read at session start. Update at session end. +## 2026-07-16 — Native Sessions production initialization hotfix + +The authenticated v0.26.0 live smoke reached Forge but the first native Hermes +send failed before contacting the runtime: the durable outbound delivery +transaction serialized `systemPrompt` before that block-scoped value was +initialized later in the handler. The prompt and optional canvas context are +now prepared before the transaction, preserving atomic user-message/outbox +writes without a production-bundle temporal-dead-zone failure. A focused +source-order regression protects this initialization invariant. + ## 2026-07-16 — v0.26.0 coordinated release preparation Release coordination confirmed that AXI-113 merged after v0.25.0 with no other @@ -119,6 +129,7 @@ crash-stranded `PROCESSING` row. The platform plugin also retains a failed final draft and reuses its original event id, preventing duplicate final messages on retry. Focused review coverage passes **33/33** TypeScript tests and **8/8** Python adapter tests. + ## 2026-07-16 — AXI-86 Artifact Studio Implemented Forge as the artifact control plane for people and agents. The @@ -482,6 +493,7 @@ Verification: focused dispatcher and operator-attention coverage passed passed; the full Vitest suite passed (**1,211 passed; 1 skipped**); a fresh production build and the responsive Command Center Playwright contract passed (**1/1** across desktop, tablet, and mobile); and `git diff --check` passed. + ## 2026-07-13 — Artifacts system research and implementation brief Created a documentation-only Artifacts audit in an isolated worktree using diff --git a/src/app/api/chat/stream/route.ts b/src/app/api/chat/stream/route.ts index ec7ec7d2..7af04591 100644 --- a/src/app/api/chat/stream/route.ts +++ b/src/app/api/chat/stream/route.ts @@ -563,6 +563,52 @@ export async function POST(req: NextRequest) { ] : body; + // Prepare connector instructions before the durable delivery transaction. + // Hermes delivery rows persist this value atomically with the user turn so + // replay has the exact original instructions. Keeping it below the + // transaction created a temporal-dead-zone failure only on the native + // Sessions path (`systemPrompt` was initialized later while the callback + // tried to serialize it). + let boundCanvasId: string | null = null; + if (canvasId) { + const canvas = await db.workspaceCanvas.findFirst({ + where: { id: canvasId, workspaceId, archivedAt: null }, + select: { id: true }, + }); + if (canvas) boundCanvasId = canvas.id; + } + + const baseSystemPrompt = + `You are ${agent.name}. You're chatting with the operator inside Forge, a project ` + + `management workspace. Be concise and direct. ` + + (agent.capabilities && agent.capabilities.length > 0 + ? `Your capabilities: ${agent.capabilities.join(", ")}.\n\n` + : "") + + (agent.templateMarkdown ? `${agent.templateMarkdown}\n` : ""); + + const buildSystemPrompt = async (): Promise => { + if (!boundCanvasId) return baseSystemPrompt; + const canvasSummary = await loadCanvasContextSummary(workspaceId, boundCanvasId); + const storyboardHint = + `When the operator asks you to lay out, storyboard, sketch, ` + + `organize, or "set up a canvas for" something, reach for the ` + + `compound MCP gestures:\n` + + `- \`canvases.storyboardPlan({ planId })\` — labeled frame with ` + + `the plan card + notes lane + sources column + next-steps lane.\n` + + `- \`canvases.storyboardIssue({ issueId })\` — labeled frame ` + + `with the issue card + related + comments + attachments.\n` + + `- \`canvases.storyboardResearch({ topic })\` — labeled frame ` + + `with a scratchpad + sources column + next-steps lane.\n` + + `- \`canvases.storyboardCustom({ name, panels })\` — escape ` + + `hatch when the three presets don't fit. Provide a panels[] ` + + `array of \`{ label, body?, x, y, width, height }\`.\n` + + `One storyboard call is the grammar — don't scatter 25 floating ` + + `nodes by hand.`; + if (!canvasSummary) return `${baseSystemPrompt}\n\n${storyboardHint}`; + return `${baseSystemPrompt}\n\n${canvasSummary}\n\n${storyboardHint}`; + }; + const systemPrompt = await buildSystemPrompt(); + // Persist the USER row and the audit/event in one transaction. Mirrors // `chat.send` so the inbox lifecycle stays honest. Forge-owned streaming // turns tag the payload `streamed: true` so the dispatch worker can no-op @@ -873,48 +919,6 @@ export async function POST(req: NextRequest) { }); const history = recent.reverse(); - // Resolve the canvas binding once up-front so an invalid id fails fast - // (rather than silently dropping the binding mid-stream). - let boundCanvasId: string | null = null; - if (canvasId) { - const canvas = await db.workspaceCanvas.findFirst({ - where: { id: canvasId, workspaceId, archivedAt: null }, - select: { id: true }, - }); - if (canvas) boundCanvasId = canvas.id; - } - - const baseSystemPrompt = - `You are ${agent.name}. You're chatting with the operator inside Forge, a project ` + - `management workspace. Be concise and direct. ` + - (agent.capabilities && agent.capabilities.length > 0 - ? `Your capabilities: ${agent.capabilities.join(", ")}.\n\n` - : "") + - (agent.templateMarkdown ? `${agent.templateMarkdown}\n` : ""); - - const buildSystemPrompt = async (): Promise => { - if (!boundCanvasId) return baseSystemPrompt; - const canvasSummary = await loadCanvasContextSummary(workspaceId, boundCanvasId); - const storyboardHint = - `When the operator asks you to lay out, storyboard, sketch, ` + - `organize, or "set up a canvas for" something, reach for the ` + - `compound MCP gestures:\n` + - `- \`canvases.storyboardPlan({ planId })\` — labeled frame with ` + - `the plan card + notes lane + sources column + next-steps lane.\n` + - `- \`canvases.storyboardIssue({ issueId })\` — labeled frame ` + - `with the issue card + related + comments + attachments.\n` + - `- \`canvases.storyboardResearch({ topic })\` — labeled frame ` + - `with a scratchpad + sources column + next-steps lane.\n` + - `- \`canvases.storyboardCustom({ name, panels })\` — escape ` + - `hatch when the three presets don't fit. Provide a panels[] ` + - `array of \`{ label, body?, x, y, width, height }\`.\n` + - `One storyboard call is the grammar — don't scatter 25 floating ` + - `nodes by hand.`; - if (!canvasSummary) return `${baseSystemPrompt}\n\n${storyboardHint}`; - return `${baseSystemPrompt}\n\n${canvasSummary}\n\n${storyboardHint}`; - }; - const systemPrompt = await buildSystemPrompt(); - // Build the model-facing message array. History rows stay plain text — // we only attach image blocks to the *latest* user turn, which is the // one we just persisted. (Historical attachments aren't replayed; the @@ -933,7 +937,7 @@ export async function POST(req: NextRequest) { messages.push({ role, content: m.body }); } } - return { boundCanvasId, buildSystemPrompt, history, messages, systemPrompt }; + return { history, messages }; })().catch(async (err) => { const failedAt = new Date().toISOString(); const message = streamErrorMessage(err, "Failed to prepare the chat turn."); @@ -963,7 +967,7 @@ export async function POST(req: NextRequest) { if (!prepared) { return NextResponse.json({ error: "Failed to prepare chat turn" }, { status: 500 }); } - const { boundCanvasId, buildSystemPrompt, history, messages, systemPrompt } = prepared; + const { history, messages } = prepared; const encoder = new TextEncoder(); const stream = new ReadableStream({ diff --git a/tests/unit/chat-stream-initialization.test.ts b/tests/unit/chat-stream-initialization.test.ts new file mode 100644 index 00000000..720c1ef2 --- /dev/null +++ b/tests/unit/chat-stream-initialization.test.ts @@ -0,0 +1,18 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("native chat stream initialization", () => { + it("prepares persisted Hermes instructions before the delivery transaction", async () => { + const source = await readFile( + path.join(process.cwd(), "src/app/api/chat/stream/route.ts"), + "utf8", + ); + const promptInitialization = source.indexOf("const systemPrompt = await buildSystemPrompt();"); + const deliveryTransaction = source.indexOf("await tx.connectorDelivery.create({"); + + expect(promptInitialization).toBeGreaterThan(-1); + expect(deliveryTransaction).toBeGreaterThan(-1); + expect(promptInitialization).toBeLessThan(deliveryTransaction); + }); +}); From 5f100f274f25fc33b414f068b26ef39b027126e8 Mon Sep 17 00:00:00 2001 From: Bailey Dixon Date: Thu, 16 Jul 2026 10:47:56 -0400 Subject: [PATCH 2/2] fix: skip unused dispatch prompt preparation --- src/app/api/chat/stream/route.ts | 4 +++- tests/unit/chat-stream-initialization.test.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/api/chat/stream/route.ts b/src/app/api/chat/stream/route.ts index 7af04591..5bc9fee9 100644 --- a/src/app/api/chat/stream/route.ts +++ b/src/app/api/chat/stream/route.ts @@ -607,7 +607,9 @@ export async function POST(req: NextRequest) { if (!canvasSummary) return `${baseSystemPrompt}\n\n${storyboardHint}`; return `${baseSystemPrompt}\n\n${canvasSummary}\n\n${storyboardHint}`; }; - const systemPrompt = await buildSystemPrompt(); + // Dispatch-only agents receive the durable user event and build their own + // runtime context. Avoid reading canvas state for a prompt Forge never uses. + const systemPrompt = useDispatch ? "" : await buildSystemPrompt(); // Persist the USER row and the audit/event in one transaction. Mirrors // `chat.send` so the inbox lifecycle stays honest. Forge-owned streaming diff --git a/tests/unit/chat-stream-initialization.test.ts b/tests/unit/chat-stream-initialization.test.ts index 720c1ef2..0a325599 100644 --- a/tests/unit/chat-stream-initialization.test.ts +++ b/tests/unit/chat-stream-initialization.test.ts @@ -8,7 +8,9 @@ describe("native chat stream initialization", () => { path.join(process.cwd(), "src/app/api/chat/stream/route.ts"), "utf8", ); - const promptInitialization = source.indexOf("const systemPrompt = await buildSystemPrompt();"); + const promptInitialization = source.indexOf( + 'const systemPrompt = useDispatch ? "" : await buildSystemPrompt();', + ); const deliveryTransaction = source.indexOf("await tx.connectorDelivery.create({"); expect(promptInitialization).toBeGreaterThan(-1);