Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b4a0789
fix(server): Claude sees composer messages as typed by the user
only21mil Oct 25, 2026
c2a26c9
test(server): Claude replay prompts carry the human origin
only21mil Oct 25, 2026
52117d9
Merge remote-tracking branch 'github/main' into maria/takeover-pr-24962
maria-rcks Oct 26, 2026
9d733cd
fix(server): attribute queued messages to their editor
maria-rcks Oct 26, 2026
71d223c
Merge remote-tracking branch 'github/main' into maria/takeover-pr-24962
maria-rcks Oct 27, 2026
90ec236
merge: sync artifact comment fix with main
maria-rcks Oct 30, 2026
6a275fb
fix(server): bind queued edits to client provenance
maria-rcks Oct 30, 2026
43ff206
fix(server): queued edits from older clients keep the message's prove…
only21mil Nov 8, 2026
fc2c1ae
fix(server): an agent's edit to a held user message keeps it held
only21mil Nov 8, 2026
eacacee
Merge branch 'main' into fix/i24131-artifact-comment-auto-replies-nev…
only21mil Nov 12, 2026
47de490
Merge branch 'main' into fix/i24131-artifact-comment-auto-replies-nev…
only21mil Nov 14, 2026
f9b530c
fix(server): a declined steer keeps a user's held message held after …
only21mil Nov 14, 2026
6e596a7
Merge remote-tracking branch 'origin/main' into fix/i24131-artifact-c…
juliusmarminge Nov 14, 2026
a6c6176
Merge branch 'main' into fix/i24131-artifact-comment-auto-replies-nev…
only21mil Nov 14, 2026
fdcf93a
fix(server): settling keeps a user's held message that an agent edited
only21mil Nov 14, 2026
9357f2f
Merge branch 'main' into fix/i24131-artifact-comment-auto-replies-nev…
only21mil Nov 14, 2026
ed8e9d4
Merge branch 'main' into fix/i24131-artifact-comment-auto-replies-nev…
only21mil Nov 14, 2026
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
1 change: 1 addition & 0 deletions apps/mobile/src/state/use-thread-composer-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ export function useThreadComposerState(input: {
const result = await editQueuedRun({
environmentId: thread.environmentId,
input: {
creationSource: "mobile",
threadId: thread.id,
runId: edit.runId,
text: restoreAttachmentOnlyBootstrapPrompt(text, payload.attachments.length > 0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ async function recordTurn(input: {
readonly label: string;
readonly pathFragments: ReadonlyArray<readonly [string, string]>;
}): Promise<SDKAssistantMessage["uuid"]> {
const message = makeClaudeUserMessage({ text: input.prompt });
const message = makeClaudeUserMessage({ text: input.prompt, origin: { kind: "human" } });
input.entries.push({
type: "expect_outbound",
label: input.label,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/mcp/toolkits/thread/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export const layer = McpToolAccess.toLayer(ThreadToolkit, {
return yield* dispatch(input.threadId, (common) => ({
...common,
type: "queued-run.edit",
editor: { createdBy: "agent", creationSource: "mcp" },
runId,
text,
}));
Expand Down
57 changes: 57 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
ProviderTurnId,
RunAttemptId,
RunId,
ScheduledTaskId,
ThreadId,
} from "@t3tools/contracts";
import { mcpAppFromToolItem } from "@t3tools/shared/toolOutput";
Expand Down Expand Up @@ -5294,6 +5295,61 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

it.effect.each([
{ name: "web", createdBy: "user", creationSource: "web", human: true },
{ name: "mobile", createdBy: "user", creationSource: "mobile", human: true },
{ name: "scheduled", createdBy: "user", creationSource: "web", scheduled: true, human: false },
{ name: "server", createdBy: "user", creationSource: "server", human: false },
{ name: "mcp", createdBy: "agent", creationSource: "mcp", human: false },
] as const)(
"stamps $name sends and steers as human only when a user typed them",
({ createdBy, creationSource, human, ...rest }) =>
Effect.gen(function* () {
const harness = yield* makeWakeHarness;
const input = makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("message-origin"),
text: "Watch the artifact",
attachments: [],
messageCreatedBy: createdBy,
messageCreationSource: creationSource,
});
const message = {
...input.message,
...("scheduled" in rest ? { scheduledTaskId: ScheduledTaskId.make("task-origin") } : {}),
};
yield* harness.runtime.startTurn({ ...input, message });
const idAllocator = yield* IdAllocator.IdAllocatorV2;
yield* harness.runtime.steerTurn({
threadId: harness.threadId,
runId: input.runId,
providerThread: harness.providerThread,
providerTurnId: idAllocator.derive.providerTurn({
driver: ClaudeAdapterV2.CLAUDE_PROVIDER,
nativeTurnId: `turn:${input.attemptId}`,
}),
message: { ...message, messageId: MessageId.make("message-origin-steer") },
});

const expected = human ? { kind: "human" } : undefined;
assert.deepEqual(harness.offeredMessages[0]?.origin, expected);
assert.equal(harness.offeredMessages[1]?.priority, "now");
assert.deepEqual(harness.offeredMessages[1]?.origin, expected);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
IdAllocator.layer,
McpProviderSessions.layer,
NodeServices.layer,
NodeCrypto.layer,
),
),
),
);

// regression: #29750 Claude sets a goal only from a prompt that opens with /goal
it("keeps a /goal that mentions a skill as one native goal command", () => {
const message = ClaudeAdapterV2.makeClaudeUserMessage({
Expand Down Expand Up @@ -10959,6 +11015,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
type: "user",
message: { role: "user", content: prompt },
parent_tool_use_id: null,
origin: { kind: "human" },
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,9 @@ function makeClaudeQueryOpenFrame(
};
}

// Recorded prompts stand in for composer messages, which T3 stamps as typed by the user.
const RECORDED_PROMPT_ORIGIN = { kind: "human" } as const;

function makeClaudePromptOfferFrame(message: SDKUserMessage): ClaudePromptOfferFrame {
return {
type: "prompt.offer",
Expand Down Expand Up @@ -1619,6 +1622,7 @@ async function recordClaudeStreamingQuery(input: {
try {
for (const [index, prompt] of input.prompts.entries()) {
const message = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: prompt,
uuid: await Effect.runPromise(
SecureRandom.SecureRandom.pipe(
Expand Down Expand Up @@ -1693,6 +1697,7 @@ async function recordClaudeActiveSteeringQuery(input: {
const promptQueue = new RecordingPromptQueue();
const offerPrompt = (index: number, priority?: SDKUserMessage["priority"]) => {
const message = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: input.prompts[index]!,
...(priority === undefined ? {} : { priority }),
});
Expand Down Expand Up @@ -1839,7 +1844,10 @@ async function recordClaudeRestartingQueries(input: {
label: `query.open:${index + 1}`,
frame: makeClaudeQueryOpenFrame({ options }),
});
const message = ClaudeAdapterV2.makeClaudeUserMessage({ text: prompt });
const message = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: prompt,
});
input.entries.push({
type: "expect_outbound",
label: `prompt.offer:${index + 1}`,
Expand Down Expand Up @@ -1941,7 +1949,10 @@ async function recordClaudeResumeAtCursorQuery(input: {
try {
const sourceCursors: Array<SDKAssistantMessage["uuid"]> = [];
for (const [index, prompt] of input.prompts.slice(0, 2).entries()) {
const message = ClaudeAdapterV2.makeClaudeUserMessage({ text: prompt });
const message = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: prompt,
});
input.entries.push({
type: "expect_outbound",
label: `prompt.offer:${index + 1}`,
Expand Down Expand Up @@ -2003,7 +2014,10 @@ async function recordClaudeResumeAtCursorQuery(input: {
label: "query.open:resume_at_cursor",
frame: makeClaudeQueryOpenFrame({ options: resumedOptions }),
});
const resumedMessage = ClaudeAdapterV2.makeClaudeUserMessage({ text: input.prompts[2]! });
const resumedMessage = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: input.prompts[2]!,
});
input.entries.push({
type: "expect_outbound",
label: "prompt.offer:3",
Expand Down Expand Up @@ -2123,7 +2137,10 @@ async function recordClaudeForkSessionQuery(input: {
try {
const sourceCursors: Array<SDKAssistantMessage["uuid"]> = [];
for (const [index, prompt] of input.prompts.slice(0, sourcePromptCount).entries()) {
const message = ClaudeAdapterV2.makeClaudeUserMessage({ text: prompt });
const message = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: prompt,
});
input.entries.push({
type: "expect_outbound",
label: `prompt.offer:${index + 1}`,
Expand Down Expand Up @@ -2225,7 +2242,10 @@ async function recordClaudeForkSessionQuery(input: {
const targetIterator = targetRuntime[Symbol.asyncIterator]();
for (const prompt of forkPrompts) {
promptOrdinal += 1;
const targetMessage = ClaudeAdapterV2.makeClaudeUserMessage({ text: prompt });
const targetMessage = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: prompt,
});
input.entries.push({
type: "expect_outbound",
label: `prompt.offer:${promptOrdinal}`,
Expand Down Expand Up @@ -2283,7 +2303,10 @@ async function recordClaudeForkSessionQuery(input: {
const continuationIterator = continuationRuntime[Symbol.asyncIterator]();
for (const prompt of input.prompts.slice(forkPromptEnd)) {
promptOrdinal += 1;
const continuationMessage = ClaudeAdapterV2.makeClaudeUserMessage({ text: prompt });
const continuationMessage = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: prompt,
});
input.entries.push({
type: "expect_outbound",
label: `prompt.offer:${promptOrdinal}`,
Expand Down Expand Up @@ -2371,7 +2394,10 @@ async function recordInterruptedClaudeQuery(input: {
options,
});
const iterator = runtime[Symbol.asyncIterator]();
const message = ClaudeAdapterV2.makeClaudeUserMessage({ text: input.prompt });
const message = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: input.prompt,
});
input.entries.push({
type: "expect_outbound",
label: input.promptOfferLabel,
Expand Down Expand Up @@ -2547,7 +2573,10 @@ async function recordClaudeInterruptRestartQuery(input: {
label: "query.open:2",
frame: makeClaudeQueryOpenFrame({ options: secondOptions }),
});
const secondMessage = ClaudeAdapterV2.makeClaudeUserMessage({ text: input.prompts[1]! });
const secondMessage = ClaudeAdapterV2.makeClaudeUserMessage({
origin: RECORDED_PROMPT_ORIGIN,
text: input.prompts[1]!,
});
input.entries.push({
type: "expect_outbound",
label: "prompt.offer:2",
Expand Down
18 changes: 18 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2744,9 +2744,21 @@ function isSupportedClaudeImageMimeType(
return supportedClaudeImageMimeTypes.has(mimeType);
}

// Claude's trust gates, like artifact comment auto-replies, need typed input stamped human.
function claudeMessageOrigin(
message: ProviderAdapter.ProviderAdapterV2TurnMessage,
): SDKUserMessage["origin"] {
return message.createdBy === "user" &&
(message.creationSource === "web" || message.creationSource === "mobile") &&
message.scheduledTaskId === undefined
? { kind: "human" }
: undefined;
}

export function makeClaudeUserMessage(input: {
readonly text: string;
readonly priority?: SDKUserMessage["priority"];
readonly origin?: SDKUserMessage["origin"];
readonly skills?: ReadonlyMap<string, ClaudeSkillTarget>;
readonly skillMentionsFrom?: number;
readonly uuid?: SDKUserMessage["uuid"];
Expand Down Expand Up @@ -2776,6 +2788,7 @@ export function makeClaudeUserMessage(input: {
},
parent_tool_use_id: null,
...(input.priority === undefined ? {} : { priority: input.priority }),
...(input.origin === undefined ? {} : { origin: input.origin }),
...(input.uuid === undefined ? {} : { uuid: input.uuid }),
};
}
Expand Down Expand Up @@ -2916,6 +2929,7 @@ const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input:
readonly text: string;
readonly attachments: ReadonlyArray<ChatAttachment>;
readonly priority?: SDKUserMessage["priority"];
readonly origin?: SDKUserMessage["origin"];
readonly uuid?: SDKUserMessage["uuid"];
readonly attachmentsDir: string;
readonly fileSystem: FileSystem.FileSystem;
Expand All @@ -2931,6 +2945,7 @@ const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input:
? {}
: { skillMentionsFrom: input.skillMentionsFrom }),
...(input.priority === undefined ? {} : { priority: input.priority }),
...(input.origin === undefined ? {} : { origin: input.origin }),
...(input.uuid === undefined ? {} : { uuid: input.uuid }),
});
const bytes = Buffer.byteLength(encodeClaudeJson(message.message.content));
Expand Down Expand Up @@ -3096,6 +3111,7 @@ const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input:
},
parent_tool_use_id: null,
...(input.priority === undefined ? {} : { priority: input.priority }),
...(input.origin === undefined ? {} : { origin: input.origin }),
...(input.uuid === undefined ? {} : { uuid: input.uuid }),
} satisfies SDKUserMessage;
});
Expand Down Expand Up @@ -12829,6 +12845,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
fileSystem,
pdfReservation,
...(skills === undefined ? {} : { skills }),
origin: claudeMessageOrigin(turnInput.message),
uuid: promptUuid,
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Expand Down Expand Up @@ -13360,6 +13377,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
),
attachments: turnInput.message.attachments,
priority: "now",
origin: claudeMessageOrigin(turnInput.message),
uuid: steerUuid,
attachmentsDir,
fileSystem,
Expand Down
Loading
Loading