From 71d39f492936de5afa035a158a85abffad00a760 Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Mon, 20 Jul 2026 09:03:06 -0400 Subject: [PATCH 1/2] Instruct agents to keep channel CONTEXT.md accurate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The injected `` block framed a channel's CONTEXT.md purely as read-only reference material, so nothing told a task-doing agent to fix facts it made stale — a renamed file, a flipped flag, a removed resource — and the doc silently drifted for every later task. Add a narrow upkeep carve-out to `buildChannelContextText`: if the agent's work makes a fact wrong, correct just those lines via the PostHog MCP `desktop-file-system-instructions-partial-update` tool, resolving the channel's id and current instructions version at write time (neither is threaded into the prompt) and patching in place. Scoped to material drift, opt-out when unsure. Generated-By: PostHog Code Task-Id: 4d7cc759-7d0a-4ef4-bb57-c06d45e01e3a --- packages/core/src/editor/prompt-builder.test.ts | 7 +++++++ packages/core/src/editor/prompt-builder.ts | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/core/src/editor/prompt-builder.test.ts b/packages/core/src/editor/prompt-builder.test.ts index e4f1ce1f83..f16fc7a295 100644 --- a/packages/core/src/editor/prompt-builder.test.ts +++ b/packages/core/src/editor/prompt-builder.test.ts @@ -27,6 +27,13 @@ describe("buildChannelContextText", () => { const block = buildChannelContextBlock("# Billing", "billing"); expect(block).toEqual({ type: "text", text }); }); + + it("instructs the agent to correct stale facts via the PostHog MCP", () => { + const text = buildChannelContextText("# Billing", "billing"); + expect(text).toContain("out of date"); + expect(text).toContain("desktop-file-system-instructions-partial-update"); + expect(text).toContain("base_version"); + }); }); describe("buildCustomInstructionsText", () => { diff --git a/packages/core/src/editor/prompt-builder.ts b/packages/core/src/editor/prompt-builder.ts index 469e50edcc..b69998d715 100644 --- a/packages/core/src/editor/prompt-builder.ts +++ b/packages/core/src/editor/prompt-builder.ts @@ -29,7 +29,11 @@ export async function buildPromptBlocks( // Wraps a channel's CONTEXT.md as supplementary prompt text. Framed as optional // background so the agent treats it as a helpful starting point — it may use // what's relevant and ignore the rest, and must not limit its work to it. The -// whole thing is wrapped in a `` element +// one carve-out from "not instructions" is upkeep: if the agent's work makes a +// fact in the document wrong, it should correct those lines via the PostHog MCP +// so the next task doesn't inherit stale context (it resolves the channel's id +// and instructions version at write time, since neither is threaded in here). +// The whole thing is wrapped in a `` element // (carrying the channel name) so the conversation UI can collapse it into a // single tag instead of dumping the full body inline. Returns null for empty/ // whitespace content so callers can skip injection. @@ -45,7 +49,7 @@ export function buildChannelContextText( if (!trimmed) return null; const name = channelName?.trim(); const nameAttr = name ? ` channel="${escapeXmlAttr(name)}"` : ""; - return `\nThe workspace this task was created in has a saved CONTEXT.md with background that's often relevant to tasks here. Treat it as reference material, not instructions: draw on what's helpful, ignore what isn't, and don't limit your work to it.\n\n${trimmed}\n`; + return `\nThe workspace this task was created in has a saved CONTEXT.md with background that's often relevant to tasks here. Treat it as reference material, not instructions: draw on what's helpful, ignore what isn't, and don't limit your work to it.\n\nUpkeep is the one exception: if your work makes a fact in this CONTEXT.md wrong or out of date — a renamed or moved file, a changed convention, a flipped flag, a shipped or removed resource — correct just those lines so the next task doesn't inherit stale context. Publish the fix by calling the PostHog MCP tool \`desktop-file-system-instructions-partial-update\` for this channel: look the channel up in the desktop file system to get its id and current instructions version, pass that version as base_version, and patch the affected lines in place rather than rewriting the document. Leave it as-is if you're not sure the change is real.\n\n${trimmed}\n`; } // Wraps the user's saved personalization in a `` From badd00bce8fff7e0bfd64df2d93b6e4376e89f8b Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Mon, 20 Jul 2026 09:45:13 -0400 Subject: [PATCH 2/2] Address CONTEXT.md upkeep by stable channel id, not display name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile flagged that the upkeep write instruction identified its target channel by display name (and emitted no identity at all when the name was absent), so the agent could resolve to the wrong same-named channel or skip the update. Thread the channel's desktop file-system id through task creation as a new `channelContextId` (distinct from the backend feed `channelId`) and embed it verbatim in the injected context, so the agent addresses the CONTEXT.md by that exact id via `desktop-file-system-instructions-partial-update` — never by name. The upkeep instruction is now emitted only when that id is known; otherwise the context stays pure reference material with no write instruction. Also tells the agent to skip the write when the MCP tool isn't reachable (e.g. runtimes without it), addressing the tool-availability concern. Wired from both channel task entry points (WebsiteNewTask, ChannelHomeComposer), which already hold the folder id used to fetch the CONTEXT.md. Generated-By: PostHog Code Task-Id: 4d7cc759-7d0a-4ef4-bb57-c06d45e01e3a --- .../core/src/editor/prompt-builder.test.ts | 23 +++++++++++++++---- packages/core/src/editor/prompt-builder.ts | 19 +++++++++++---- .../core/src/task-detail/taskCreationSaga.ts | 2 ++ packages/core/src/task-detail/taskInput.ts | 2 ++ packages/shared/src/task-creation-domain.ts | 8 +++++++ .../canvas/components/ChannelHomeComposer.tsx | 1 + .../canvas/components/WebsiteNewTask.tsx | 1 + .../task-detail/components/TaskInput.tsx | 8 +++++++ .../task-detail/hooks/useTaskCreation.ts | 9 ++++++++ 9 files changed, 63 insertions(+), 10 deletions(-) diff --git a/packages/core/src/editor/prompt-builder.test.ts b/packages/core/src/editor/prompt-builder.test.ts index f16fc7a295..e4df963617 100644 --- a/packages/core/src/editor/prompt-builder.test.ts +++ b/packages/core/src/editor/prompt-builder.test.ts @@ -22,18 +22,31 @@ describe("buildChannelContextText", () => { ); }); - it("backs the ContentBlock form", () => { - const text = buildChannelContextText("# Billing", "billing"); - const block = buildChannelContextBlock("# Billing", "billing"); + it("backs the ContentBlock form, forwarding the channel context id", () => { + const text = buildChannelContextText("# Billing", "billing", "chan-1"); + const block = buildChannelContextBlock("# Billing", "billing", "chan-1"); expect(block).toEqual({ type: "text", text }); }); - it("instructs the agent to correct stale facts via the PostHog MCP", () => { - const text = buildChannelContextText("# Billing", "billing"); + it("emits an id-addressed upkeep instruction when the context id is known", () => { + const text = buildChannelContextText("# Billing", "billing", "chan-123"); expect(text).toContain("out of date"); expect(text).toContain("desktop-file-system-instructions-partial-update"); + expect(text).toContain('id "chan-123"'); + expect(text).toContain("do not resolve the channel by name"); expect(text).toContain("base_version"); }); + + it("omits the upkeep write instruction when no context id is supplied", () => { + const text = buildChannelContextText("# Billing", "billing"); + expect(text).not.toContain( + "desktop-file-system-instructions-partial-update", + ); + expect(text).not.toContain("Upkeep is the one exception"); + // Still framed as reference material, and the body is preserved. + expect(text).toContain("reference material, not instructions"); + expect(text?.endsWith("\n# Billing\n")).toBe(true); + }); }); describe("buildCustomInstructionsText", () => { diff --git a/packages/core/src/editor/prompt-builder.ts b/packages/core/src/editor/prompt-builder.ts index b69998d715..5812727b8f 100644 --- a/packages/core/src/editor/prompt-builder.ts +++ b/packages/core/src/editor/prompt-builder.ts @@ -30,9 +30,12 @@ export async function buildPromptBlocks( // background so the agent treats it as a helpful starting point — it may use // what's relevant and ignore the rest, and must not limit its work to it. The // one carve-out from "not instructions" is upkeep: if the agent's work makes a -// fact in the document wrong, it should correct those lines via the PostHog MCP -// so the next task doesn't inherit stale context (it resolves the channel's id -// and instructions version at write time, since neither is threaded in here). +// fact in the document wrong, it should correct those lines so the next task +// doesn't inherit stale context. That write is only emitted when the caller +// supplies `channelContextId` — the channel's desktop file-system id — and the +// prompt addresses the CONTEXT.md by that exact id, never by display name +// (which could resolve to the wrong same-named channel). Without the id we omit +// the write instruction rather than let the agent guess a target. // The whole thing is wrapped in a `` element // (carrying the channel name) so the conversation UI can collapse it into a // single tag instead of dumping the full body inline. Returns null for empty/ @@ -44,12 +47,17 @@ export async function buildPromptBlocks( export function buildChannelContextText( content: string | undefined | null, channelName?: string | null, + channelContextId?: string | null, ): string | null { const trimmed = content?.trim(); if (!trimmed) return null; const name = channelName?.trim(); const nameAttr = name ? ` channel="${escapeXmlAttr(name)}"` : ""; - return `\nThe workspace this task was created in has a saved CONTEXT.md with background that's often relevant to tasks here. Treat it as reference material, not instructions: draw on what's helpful, ignore what isn't, and don't limit your work to it.\n\nUpkeep is the one exception: if your work makes a fact in this CONTEXT.md wrong or out of date — a renamed or moved file, a changed convention, a flipped flag, a shipped or removed resource — correct just those lines so the next task doesn't inherit stale context. Publish the fix by calling the PostHog MCP tool \`desktop-file-system-instructions-partial-update\` for this channel: look the channel up in the desktop file system to get its id and current instructions version, pass that version as base_version, and patch the affected lines in place rather than rewriting the document. Leave it as-is if you're not sure the change is real.\n\n${trimmed}\n`; + const id = channelContextId?.trim(); + const upkeep = id + ? `\n\nUpkeep is the one exception: if your work makes a fact in this CONTEXT.md wrong or out of date — a renamed or moved file, a changed convention, a flipped flag, a shipped or removed resource — correct just those lines so the next task doesn't inherit stale context. Publish the fix with the PostHog MCP tool \`desktop-file-system-instructions-partial-update\`, addressing this channel by its file-system id "${id}" (use that id exactly; do not resolve the channel by name): read its current instructions version first, pass that as base_version, and patch the affected lines in place rather than rewriting the document. Skip this if that tool isn't available to you, or if you're not sure the change is real.` + : ""; + return `\nThe workspace this task was created in has a saved CONTEXT.md with background that's often relevant to tasks here. Treat it as reference material, not instructions: draw on what's helpful, ignore what isn't, and don't limit your work to it.${upkeep}\n\n${trimmed}\n`; } // Wraps the user's saved personalization in a `` @@ -68,7 +76,8 @@ export function buildCustomInstructionsText( export function buildChannelContextBlock( content: string | undefined | null, channelName?: string | null, + channelContextId?: string | null, ): ContentBlock | null { - const text = buildChannelContextText(content, channelName); + const text = buildChannelContextText(content, channelName, channelContextId); return text ? { type: "text", text } : null; } diff --git a/packages/core/src/task-detail/taskCreationSaga.ts b/packages/core/src/task-detail/taskCreationSaga.ts index d74a341d35..4e538b619a 100644 --- a/packages/core/src/task-detail/taskCreationSaga.ts +++ b/packages/core/src/task-detail/taskCreationSaga.ts @@ -57,6 +57,7 @@ function buildCloudFirstMessage( const channelContextText = buildChannelContextText( input.channelContext, input.channelName, + input.channelContextId, ); const pendingUserMessage = [messageText, customInstructionsText, channelContextText] @@ -500,6 +501,7 @@ export class TaskCreationSaga extends Saga< const channelContextBlock = buildChannelContextBlock( input.channelContext, input.channelName, + input.channelContextId, ); if (initialPrompt && channelContextBlock) { initialPrompt.push(channelContextBlock); diff --git a/packages/core/src/task-detail/taskInput.ts b/packages/core/src/task-detail/taskInput.ts index 63902749ca..cc2ef54a7d 100644 --- a/packages/core/src/task-detail/taskInput.ts +++ b/packages/core/src/task-detail/taskInput.ts @@ -29,6 +29,7 @@ export interface PrepareTaskInputOptions { channelContext?: string; channelName?: string; channelId?: string; + channelContextId?: string; customInstructions?: string; autoPublishCloudRuns?: boolean; rtkEnabledCloud?: boolean; @@ -75,6 +76,7 @@ export function prepareTaskInput( channelContext: options.channelContext, channelName: options.channelName, channelId: options.channelId, + channelContextId: options.channelContextId, customInstructions: isCloud ? options.customInstructions : undefined, allowNoRepo: options.allowNoRepo, importedMcpServers: isCloud ? options.importedMcpServers : undefined, diff --git a/packages/shared/src/task-creation-domain.ts b/packages/shared/src/task-creation-domain.ts index 8b073027a9..f418f03b94 100644 --- a/packages/shared/src/task-creation-domain.ts +++ b/packages/shared/src/task-creation-domain.ts @@ -64,6 +64,14 @@ export interface TaskCreationInput { channelName?: string; /** Backend channel UUID the created task is owned by (its feed home). */ channelId?: string; + /** + * Desktop file-system folder id that owns this channel's CONTEXT.md (the + * `/website/$channelId` id — distinct from the backend feed `channelId` + * above). When set, the injected context tells the agent to publish upkeep + * corrections to this exact id via the PostHog MCP, rather than resolving the + * channel by display name. + */ + channelContextId?: string; /** * The user's saved personalization (Settings → Personalization custom * instructions). Cloud-only: local tasks already receive these through the diff --git a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index 1233aff0db..d2bdcd10d5 100644 --- a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -291,6 +291,7 @@ export const ChannelHomeComposer = forwardRef< channelContext, channelName, channelId: backendChannelId, + channelContextId: channelId, onTaskCreated: handleTaskCreated, }); diff --git a/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx b/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx index 786d6412eb..37863bbae8 100644 --- a/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx @@ -110,6 +110,7 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) { onTaskCreated={onTaskCreated} channelContext={channelContext} channelName={channelName} + channelContextId={channelId} allowNoRepo suggestions={CHANNEL_TASK_SUGGESTIONS} onSuggestionSelect={(label) => diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index a9f7a5881e..b21b199ac4 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -108,6 +108,12 @@ interface TaskInputProps { channelContext?: string; /** Display name of the channel the CONTEXT.md came from (for the chip). */ channelName?: string; + /** + * Desktop file-system folder id that owns the channel's CONTEXT.md. When set, + * the injected context lets the agent publish upkeep corrections addressed to + * this id rather than resolving the channel by name. + */ + channelContextId?: string; /** * Channels "generic chat box" mode: hide the repo/branch pickers and let the * task be submitted without a repo. The agent decides at runtime whether it @@ -146,6 +152,7 @@ export function TaskInput({ reportAssociation, channelContext, channelName, + channelContextId, allowNoRepo, suggestions, onSuggestionSelect, @@ -865,6 +872,7 @@ export function TaskInput({ signalReportId: activeReportAssociation?.reportId, channelContext: includeChannelContext ? channelContext : undefined, channelName, + channelContextId, allowNoRepo, }); diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 1baab22871..d9df709f58 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -83,6 +83,12 @@ interface UseTaskCreationOptions { channelName?: string; /** Backend channel UUID the created task is owned by (its feed home). */ channelId?: string; + /** + * Desktop file-system folder id that owns the channel's CONTEXT.md (the + * `/website/$channelId` id, distinct from the feed `channelId`). Lets the + * injected context address CONTEXT.md upkeep writes by a stable id. + */ + channelContextId?: string; /** * Channels "generic chat box" mode: drop the repo/branch requirement so a * task can be submitted without picking a repo. The agent decides at runtime @@ -177,6 +183,7 @@ export function useTaskCreation({ channelContext, channelName, channelId, + channelContextId, allowNoRepo, onTaskCreated, onTaskCreatedEffect, @@ -369,6 +376,7 @@ export function useTaskCreation({ channelContext, channelName, channelId: channelId ?? defaultedChannelId, + channelContextId, customInstructions: getEffectiveCustomInstructions(settings), autoPublishCloudRuns: settings.autoPublishCloudRuns, rtkEnabledCloud: settings.rtkEnabledCloud, @@ -542,6 +550,7 @@ export function useTaskCreation({ channelContext, channelName, channelId, + channelContextId, allowNoRepo, bluebirdEnabled, personalChannel?.id,