Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
94 changes: 50 additions & 44 deletions src/app/api/chat/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,54 @@ 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<string> => {
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}`;
};
// 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
// turns tag the payload `streamed: true` so the dispatch worker can no-op
Expand Down Expand Up @@ -873,48 +921,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<string> => {
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
Expand All @@ -933,7 +939,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.");
Expand Down Expand Up @@ -963,7 +969,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<Uint8Array>({
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/chat-stream-initialization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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 = useDispatch ? "" : await buildSystemPrompt();',
);
const deliveryTransaction = source.indexOf("await tx.connectorDelivery.create({");

expect(promptInitialization).toBeGreaterThan(-1);
expect(deliveryTransaction).toBeGreaterThan(-1);
expect(promptInitialization).toBeLessThan(deliveryTransaction);
});
});
Loading