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
47 changes: 32 additions & 15 deletions extensions/gentle-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9796,6 +9796,35 @@ function createGentleAiExtensionForTesting(
reminderEpoch += 1;
unbindPreparation?.();
reminderManager = ctx.sessionManager;
// gentle-shell#1690: a delegated child runs in the parent's resolved
// worktree. Repository preparation, review negotiation, asset install and
// model config belong to the parent session and write shared state. The
// standing review grant is host-only (a child never captures an identity),
// so revoke/refresh have nothing to act on; the child relay is load-time.
// Only that parent-owned work is skipped: the session-local resets above,
// the dev-binary notice, and any step added after this call, still run in
// children.
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1") await startParentSession(event, ctx);
else await surfaceDevBinaryOverride(ctx);
});

// Loud, every session: an active dev-binary override means this session
// runs an unpinned gentle-ai. One visible startup notice: the gentle-shell
// 🌹 card owns the announcement when it can render (shell enabled with UI);
// this toast is only the fallback for when the card is unavailable. The
// hasUI guard stays: headless contexts have no toast to show.
const surfaceDevBinaryOverride = async (ctx: ExtensionContext): Promise<void> => {
const devBinaryToastFallback = ctx.hasUI && !shellEnabled();
try {
const devBinary = await describeDevBinaryOverride();
if (devBinaryToastFallback && devBinary.state === "active") ctx.ui.notify(devBinary.line, "warning");
if (devBinaryToastFallback && devBinary.state === "invalid") ctx.ui.notify(devBinary.line, "error");
} catch (error) {
if (ctx.hasUI) ctx.ui.notify(`Gentle AI dev binary override check failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
}
};

const startParentSession = async (event: unknown, ctx: ExtensionContext): Promise<void> => {
const epoch = reminderEpoch;
const manager = ctx.sessionManager;
const originalCwd = manager?.getCwd?.() ?? ctx.cwd;
Expand All @@ -9816,19 +9845,7 @@ function createGentleAiExtensionForTesting(
const reason = (event as { reason?: unknown }).reason;
if (reason !== "reload") revokeCurrentReviewSessionPermission(ctx);
await refreshReviewSessionPermissionStatus(ctx);
// Loud, every session: an active dev-binary override means this session
// runs an unpinned gentle-ai. One visible startup notice: the gentle-shell
// 🌹 card owns the announcement when it can render (shell enabled with UI);
// this toast is only the fallback for when the card is unavailable. The
// hasUI guard stays: headless contexts have no toast to show.
const devBinaryToastFallback = ctx.hasUI && !shellEnabled();
try {
const devBinary = await describeDevBinaryOverride();
if (devBinaryToastFallback && devBinary.state === "active") ctx.ui.notify(devBinary.line, "warning");
if (devBinaryToastFallback && devBinary.state === "invalid") ctx.ui.notify(devBinary.line, "error");
} catch (error) {
if (ctx.hasUI) ctx.ui.notify(`Gentle AI dev binary override check failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
}
await surfaceDevBinaryOverride(ctx);
try {
const installResult = installPackageAssets(ctx.cwd, true, ["delegation", "review"]);
migrateLegacyProjectModelOverrides(ctx.cwd);
Expand Down Expand Up @@ -9864,7 +9881,7 @@ function createGentleAiExtensionForTesting(
} catch {
// Startup negotiation is best-effort only; never surface or throw.
}
});
};

pi.on("before_agent_start", async (event, ctx) => {
const isNamedAgent = isNamedAgentStartEvent(event);
Expand Down Expand Up @@ -9984,7 +10001,7 @@ function createGentleAiExtensionForTesting(
// Persist the observed own write before any await. Preparation is not
// mutation evidence, and cannot invent a pre-write Changes baseline.
if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
if (prospectiveRoot && !resolveSessionWorktree(ctx.cwd, ctx.cwd)) await prepareBoundSessionRepository(ctx.sessionManager, ctx.sessionManager.getCwd?.() ?? ctx.cwd, ctx.signal);
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && prospectiveRoot && !resolveSessionWorktree(ctx.cwd, ctx.cwd)) await prepareBoundSessionRepository(ctx.sessionManager, ctx.sessionManager.getCwd?.() ?? ctx.cwd, ctx.signal);
} catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
});

Expand Down
6 changes: 4 additions & 2 deletions extensions/history/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1316,8 +1316,10 @@ export default function promptHistoryExtension(
): void {
const env = deps.env ?? process.env;
const configHome = deps.gentlePiConfigHome ?? gentlePiConfigHome(env);
// Per-prompt gate: re-read so a Customize toggle applies live.
const capturing = () => captureEnabled(env, configHome);
// Per-prompt gate: re-read so a Customize toggle applies live. A delegated
// child never captures: its prompt is a delegation brief, not user history,
// and the parent owns the store's init and GC (gentle-shell#1690).
const capturing = () => env.GENTLE_PI_AGENTS_CHILD !== "1" && captureEnabled(env, configHome);
const root = deps.root ?? PI_HISTORY_ROOT;
const cwd = deps.cwd ?? process.cwd();
const instanceId = deps.instanceId ?? randomUUID();
Expand Down
3 changes: 3 additions & 0 deletions extensions/pi-pretty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default async function gentlePiPrettyExtension(
bundled?: PiPrettyExtension,
env: NodeJS.ProcessEnv = process.env,
): Promise<unknown> {
// gentle-shell#1690: a delegated child has no transcript to prettify, and
// the upstream fallback would start its own file indexing in every child.
if (env.GENTLE_PI_AGENTS_CHILD === "1") return undefined;
if (quietToolsEnabled()) {
process.env.PRETTY_DISABLE_TOOLS = mergeDisabledTools(
process.env.PRETTY_DISABLE_TOOLS,
Expand Down
3 changes: 3 additions & 0 deletions extensions/skill-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,9 @@ function shouldSkipSkillRegistryStartup(
env = process.env,
): boolean {
return (
// gentle-shell#1690: delegated children share the parent's cwd; the
// parent owns .atl/ writes, the legacy rename and the watcher.
env.GENTLE_PI_AGENTS_CHILD === "1" ||
pi.getFlag(NO_SKILL_REGISTRY_FLAG) === true ||
isTruthyEnv(env[NO_SKILL_REGISTRY_ENV]) ||
hasCliArg(argv, "--no-skills", "-ns")
Expand Down
3 changes: 3 additions & 0 deletions extensions/startup-banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,9 @@ export default function (pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
disposeHeader();
if (!ctx.hasUI) return;
// Delegated rpc children report hasUI=true but have no terminal to paint
// (gentle-shell#1690); do not rely on a piped stdout lacking rows/columns.
if (process.env.GENTLE_PI_AGENTS_CHILD === "1") return;

// CLI subcommands such as `pi update` or `pi install` skip the animated intro.
if (isPiCliSubcommandInvocation(process.argv)) return;
Expand Down
Loading
Loading