Skip to content
Open
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
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -591,11 +591,7 @@ jobs:
name: Mobile Native Static Analysis
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.mobile_native != 'false' }}
<<<<<<< origin/main
runs-on: macos-26
=======
runs-on: blacksmith-6vcpu-macos-27
>>>>>>> upstream/main
timeout-minutes: 10
steps:
- name: Checkout
Expand Down
9 changes: 9 additions & 0 deletions apps/server/src/cli/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Command } from "effect/cli";
import { ChildProcessSpawner } from "effect/process";

import packageJson from "../../package.json" with { type: "json" };
import { HEADLESS_SHELL_PROFILE } from "../fork/previewChrome.ts";
import * as PreviewBrowserHost from "../preview/PreviewBrowserHost.ts";
import { browserCommand } from "./browser.ts";

Expand All @@ -32,6 +33,8 @@ const runSetupAsRoot = (input: { readonly profile: string | undefined }) => {
const files = new Map([
[USERNS_RESTRICTION, "1\n"],
[BROWSER, ""],
// The fork's setup also asks for Xvfb, which full Chrome's headed mode runs in.
["/usr/bin/Xvfb", ""],
]);
if (input.profile !== undefined) {
files.set(PreviewBrowserHost.APPARMOR_PROFILE_PATH, input.profile);
Expand Down Expand Up @@ -122,6 +125,12 @@ it.effect.each([
written: PreviewBrowserHost.APPARMOR_PROFILE,
said: "Wrote and loaded",
},
{
label: "rewrites the profile an older T3 wrote",
profile: HEADLESS_SHELL_PROFILE,
written: PreviewBrowserHost.APPARMOR_PROFILE,
said: "Wrote and loaded",
},
{
label: "reloads a customized profile without rewriting it",
profile: CUSTOMIZED_PROFILE,
Expand Down
12 changes: 5 additions & 7 deletions apps/server/src/cli/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as ChildProcess from "effect/process/ChildProcess";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";

import { browserSetupRootWarning } from "../fork/browserSetupRootWarning.ts";
import { FULL_CHROME_PRODUCT } from "../fork/previewChrome.ts";
import { FULL_CHROME_PRODUCT, profileNeedsWrite } from "../fork/previewChrome.ts";
import { missingDisplayServer } from "../fork/previewDisplay.ts";
import * as PreviewBrowserHost from "../preview/PreviewBrowserHost.ts";
import { resolveBaseDir } from "../os-jank.ts";
Expand Down Expand Up @@ -175,11 +175,9 @@ const browserSetupCommand = Command.make("setup", { baseDir: baseDirFlag }).pipe
}

if (needsProfile) {
// An existing profile may carry an administrator's edits, so it is reloaded as is.
const profileOnDisk = yield* fs
.exists(PreviewBrowserHost.APPARMOR_PROFILE_PATH)
.pipe(Effect.orElseSucceed(() => false));
if (!profileOnDisk) {
// Any profile but an older T3's may carry an administrator's edits, so it is reloaded as is.
const keepProfile = !(yield* profileNeedsWrite(PreviewBrowserHost.APPARMOR_PROFILE_PATH));
if (!keepProfile) {
yield* fs
.writeFileString(
PreviewBrowserHost.APPARMOR_PROFILE_PATH,
Expand All @@ -200,7 +198,7 @@ const browserSetupCommand = Command.make("setup", { baseDir: baseDirFlag }).pipe
PreviewBrowserHost.APPARMOR_PROFILE_PATH,
]);
yield* Console.log(
profileOnDisk
keepProfile
? `Reloaded the AppArmor profile at ${PreviewBrowserHost.APPARMOR_PROFILE_PATH} that allows Chrome's sandbox for T3's browser.`
: `Wrote and loaded the AppArmor profile at ${PreviewBrowserHost.APPARMOR_PROFILE_PATH} that allows Chrome's sandbox for T3's browser.`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { assert, it } from "@effect/vitest";
import {
ClaudeSettings,
CodexSettings,
EnvironmentId,
MessageId,
NodeId,
ProjectId,
Expand Down Expand Up @@ -221,6 +222,53 @@ it.effect("a Claude adapter built on the provider infrastructure adds the thread
}).pipe(Effect.scoped, Effect.provide(infrastructureFor(threadId, "t3-compaction-note-claude-")));
});

it.effect(
"Claude gets upstream's send_to_user section from the hook in Normal, not in its system prompt",
() => {
const normalId = ThreadId.make("thread:send-section:normal");
const messagesOnlyId = ThreadId.make("thread:send-section:messages-only");
const section = "### Text the user must read mid-turn";
return Effect.gen(function* () {
const sessions = yield* McpProviderSessions.McpProviderSessions;
const submitted = Effect.fn("submitted")(function* (threadId: ThreadId) {
yield* sessions.set({
environmentId: EnvironmentId.make("environment-send-section"),
threadId,
providerSessionId: "mcp-session-send-section",
providerInstanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
endpoint: "http://127.0.0.1:43123/mcp",
authorizationHeader: "Bearer send-section",
browserToolsAvailable: false,
});
const options = yield* startClaudeTurn(threadId, { text: "hi" });
const append = (options.systemPrompt as { readonly append?: string }).append ?? "";
assert.include(append, "call `t3_background_wait`");
assert.notInclude(append, section);
const callback = options.hooks?.UserPromptSubmit?.[0]?.hooks[0];
const output = yield* Effect.promise(() =>
callback!(
{
hook_event_name: "UserPromptSubmit",
prompt: "hi",
session_id: "native-thread-compaction-note",
transcript_path: "/transcripts/native-thread-compaction-note.jsonl",
cwd: "/workspace",
} satisfies HookInput,
undefined,
{ signal: new AbortController().signal },
),
);
return "hookSpecificOutput" in output &&
output.hookSpecificOutput?.hookEventName === "UserPromptSubmit"
? output.hookSpecificOutput.additionalContext
: undefined;
});
assert.include(yield* submitted(normalId), section);
assert.isUndefined(yield* submitted(messagesOnlyId));
}).pipe(Effect.scoped, Effect.provide(infrastructureFor(messagesOnlyId, "t3-send-section-")));
},
);

it.effect("Claude gets a slash command's note only with that command's own prompt", () => {
const threadId = ThreadId.make("thread:command-note:claude");
return Effect.gen(function* () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ it.layer(notes)("DisplayModeCompactionNote", (it) => {
assert.deepEqual(yield* toldRows, before);
}),
);

it.effect("reads whether a thread is in Normal", () =>
Effect.gen(function* () {
const { isNormal } = yield* DisplayModeCompactionNote.DisplayModeCompactionNote;
assert.isTrue(yield* isNormal(plainId));
assert.isFalse(yield* isNormal(messagesOnlyId));
}),
);
});

it("states the mode instead of announcing a switch", () => {
Expand All @@ -74,6 +82,7 @@ it("adds the compaction note hook to Claude beside the subagent hook", async ()
notes: {
noteAfterCompaction: (id) =>
Effect.succeed(id === threadId ? MESSAGES_ONLY_DISPLAY_MODE.stateNotice : undefined),
isNormal: () => Effect.succeed(true),
},
threadId,
activeTurn: Effect.undefined,
Expand Down Expand Up @@ -101,3 +110,57 @@ it("adds the compaction note hook to Claude beside the subagent hook", async ()
});
assert.deepEqual(await callback({ ...input, agent_id: "subagent-1" }, undefined, { signal }), {});
});

it("gives upstream's send_to_user section once in Normal, again after another mode or a compaction", async () => {
const threadId = ThreadId.make("thread:compaction-note:section");
let normal: boolean | undefined = true;
const hooks = DisplayModeCompactionNote.claudeDisplayModeHooks({
notes: {
noteAfterCompaction: () => Effect.undefined,
isNormal: () => Effect.succeed(normal),
},
threadId,
activeTurn: Effect.undefined,
runPromise: Effect.runPromise,
sendToUserSection: "SECTION",
});
const prompt = hooks.UserPromptSubmit![0]!.hooks[0]!;
const compact = hooks.SessionStart![0]!.hooks[0]!;
const base = {
session_id: "native-thread-section",
transcript_path: "/transcripts/native-thread-section.jsonl",
cwd: "/workspace",
};
const signal = new AbortController().signal;
const submit = async (extra: Partial<HookInput> = {}) => {
const output = await prompt(
{ ...base, hook_event_name: "UserPromptSubmit", prompt: "Go on.", ...extra } as HookInput,
undefined,
{ signal },
);
return "hookSpecificOutput" in output &&
output.hookSpecificOutput?.hookEventName === "UserPromptSubmit"
? output.hookSpecificOutput.additionalContext
: undefined;
};

assert.isUndefined(await submit({ source: "system" } as Partial<HookInput>));
assert.isUndefined(await submit({ agent_id: "subagent-1" }));
assert.equal(await submit(), "SECTION");
assert.isUndefined(await submit());
normal = undefined;
assert.isUndefined(await submit());
normal = false;
assert.isUndefined(await submit());
normal = true;
assert.equal(await submit(), "SECTION");
const compacted = await compact(
{ ...base, hook_event_name: "SessionStart", source: "compact" } satisfies HookInput,
undefined,
{ signal },
);
assert.deepEqual(compacted, {
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: "SECTION" },
});
assert.isUndefined(await submit());
});
71 changes: 56 additions & 15 deletions apps/server/src/fork/displayModeWrap/DisplayModeCompactionNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface DisplayModeCompactionNoteShape {
* and returns `undefined`.
*/
readonly noteAfterCompaction: (threadId: ThreadId) => Effect.Effect<string | undefined>;
/** Whether the thread is in Normal, or `undefined` when the read fails, which it logs. */
readonly isNormal: (threadId: ThreadId) => Effect.Effect<boolean | undefined>;
}

export class DisplayModeCompactionNote extends Context.Service<
Expand Down Expand Up @@ -69,7 +71,24 @@ export const layer: Layer.Layer<
);
});

return DisplayModeCompactionNote.of({ noteAfterCompaction });
const isNormal: DisplayModeCompactionNoteShape["isNormal"] = Effect.fn(
"DisplayModeCompactionNote.isNormal",
)(function* (threadId) {
return yield* serverSettings.getSettings.pipe(
Effect.map(
(settings) =>
resolveDisplayMode(settings.threadDisplayModes[threadId]) === NORMAL_DISPLAY_MODE,
),
Effect.catchCause((cause) =>
Effect.logWarning("Display mode read for the send_to_user section failed", {
threadId,
cause,
}).pipe(Effect.as(undefined)),
),
);
});

return DisplayModeCompactionNote.of({ noteAfterCompaction, isNormal });
}),
);

Expand All @@ -84,8 +103,23 @@ export const claudeDisplayModeHooks = (input: {
readonly threadId: ThreadId;
readonly activeTurn: Effect.Effect<ProviderAdapter.ProviderAdapterV2TurnInput | undefined>;
readonly runPromise: <A>(effect: Effect.Effect<A>) => Promise<A>;
/**
* Upstream's send_to_user section of the system prompt, which a conversation keeps from its start.
* In each Claude process, the first user prompt in Normal gets it, a compaction in Normal gets it
* again, and so does the first Normal prompt after another mode's prompt.
*/
readonly sendToUserSection?: string;
}): Partial<Record<HookEvent, HookCallbackMatcher[]>> => {
const { notes, threadId, activeTurn, runPromise } = input;
const { notes, threadId, activeTurn, runPromise, sendToUserSection } = input;
let sectionOwed = true;
const section = async () => {
if (sendToUserSection === undefined || notes === undefined) return undefined;
const normal = await runPromise(notes.isNormal(threadId));
if (normal === false) sectionOwed = true;
if (normal !== true || !sectionOwed) return undefined;
sectionOwed = false;
return sendToUserSection;
};
return notes === undefined
? {}
: {
Expand All @@ -95,20 +129,25 @@ export const claudeDisplayModeHooks = (input: {
async (hookInput) => {
if (
hookInput.hook_event_name !== "UserPromptSubmit" ||
hookInput.source === "system"
hookInput.source === "system" ||
hookInput.agent_id !== undefined
)
return {};
const turn = await runPromise(activeTurn);
const note = turn?.commandNote;
if (note === undefined || hookInput.prompt.trim() !== turn?.message.text.trim())
return {};
await runPromise(note.delivered);
return {
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: note.text,
},
};
const note =
hookInput.prompt.trim() === turn?.message.text.trim()
? turn.commandNote
: undefined;
if (note !== undefined) await runPromise(note.delivered);
const context = [note?.text, await section()].filter((text) => text !== undefined);
return context.length === 0
? {}
: {
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: context.join("\n\n"),
},
};
},
],
},
Expand All @@ -119,13 +158,15 @@ export const claudeDisplayModeHooks = (input: {
hooks: [
async (input) => {
if (input.agent_id !== undefined) return {};
sectionOwed = true;
const note = await runPromise(notes.noteAfterCompaction(threadId));
return note === undefined
const context = [note, await section()].filter((text) => text !== undefined);
return context.length === 0
? {}
: {
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: note,
additionalContext: context.join("\n\n"),
},
};
},
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/fork/generalRoleParity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { DeviceService } from "../device/DeviceService.ts";
import { DisplayModeWrap } from "./displayModeWrap/DisplayModeWrap.ts";
import { ThreadDashboard } from "./threadDashboard/ThreadDashboard.ts";
import { ServerEnvironment } from "../environment/ServerEnvironment.ts";
import * as EnvironmentTheme from "../environmentTheme.ts";
import { GitWorkflowService } from "../git/GitWorkflowService.ts";
import * as McpHttpServer from "../mcp/McpHttpServer.ts";
import type { McpThreadInvocationScope } from "../mcp/McpInvocationContext.ts";
Expand Down Expand Up @@ -649,6 +650,7 @@ const toolHandlerServices = Layer.mergeAll(
Layer.mock(ThreadDashboard)({}),
Layer.mock(DisplayModeWrap)({ forgetNotesAfterCompaction: () => (ingest) => ingest }),
Layer.mock(DocumentText)({}),
Layer.mock(EnvironmentTheme.EnvironmentThemeService)({}),
Layer.mock(GitVcsDriver.GitVcsDriver)({}),
Layer.mock(GitWorkflowService)({}),
Layer.mock(ManagedProjectFolders)({ namedProjectsRoot: "" }),
Expand Down
Loading
Loading