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
26 changes: 23 additions & 3 deletions packages/core/src/editor/prompt-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +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("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</channel_context>")).toBe(true);
});
});

describe("buildCustomInstructionsText", () => {
Expand Down
19 changes: 16 additions & 3 deletions packages/core/src/editor/prompt-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ 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 `<channel_context channel="...">` 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 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 `<channel_context channel="...">` 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.
Expand All @@ -40,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 `<channel_context${nameAttr}>\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</channel_context>`;
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 `<channel_context${nameAttr}>\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</channel_context>`;
}

// Wraps the user's saved personalization in a `<user_custom_instructions>`
Expand All @@ -64,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;
}
2 changes: 2 additions & 0 deletions packages/core/src/task-detail/taskCreationSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function buildCloudFirstMessage(
const channelContextText = buildChannelContextText(
input.channelContext,
input.channelName,
input.channelContextId,
);
const pendingUserMessage =
[messageText, customInstructionsText, channelContextText]
Expand Down Expand Up @@ -500,6 +501,7 @@ export class TaskCreationSaga extends Saga<
const channelContextBlock = buildChannelContextBlock(
input.channelContext,
input.channelName,
input.channelContextId,
);
if (initialPrompt && channelContextBlock) {
initialPrompt.push(channelContextBlock);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/task-detail/taskInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface PrepareTaskInputOptions {
channelContext?: string;
channelName?: string;
channelId?: string;
channelContextId?: string;
customInstructions?: string;
autoPublishCloudRuns?: boolean;
rtkEnabledCloud?: boolean;
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/task-creation-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export const ChannelHomeComposer = forwardRef<
channelContext,
channelName,
channelId: backendChannelId,
channelContextId: channelId,
onTaskCreated: handleTaskCreated,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/src/features/task-detail/components/TaskInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -146,6 +152,7 @@ export function TaskInput({
reportAssociation,
channelContext,
channelName,
channelContextId,
allowNoRepo,
suggestions,
onSuggestionSelect,
Expand Down Expand Up @@ -865,6 +872,7 @@ export function TaskInput({
signalReportId: activeReportAssociation?.reportId,
channelContext: includeChannelContext ? channelContext : undefined,
channelName,
channelContextId,
allowNoRepo,
});

Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/features/task-detail/hooks/useTaskCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -177,6 +183,7 @@ export function useTaskCreation({
channelContext,
channelName,
channelId,
channelContextId,
allowNoRepo,
onTaskCreated,
onTaskCreatedEffect,
Expand Down Expand Up @@ -369,6 +376,7 @@ export function useTaskCreation({
channelContext,
channelName,
channelId: channelId ?? defaultedChannelId,
channelContextId,
customInstructions: getEffectiveCustomInstructions(settings),
autoPublishCloudRuns: settings.autoPublishCloudRuns,
rtkEnabledCloud: settings.rtkEnabledCloud,
Expand Down Expand Up @@ -542,6 +550,7 @@ export function useTaskCreation({
channelContext,
channelName,
channelId,
channelContextId,
allowNoRepo,
bluebirdEnabled,
personalChannel?.id,
Expand Down
Loading