diff --git a/apps/server/src/mcp/toolkits/pair/handlers.test.ts b/apps/server/src/mcp/toolkits/pair/handlers.test.ts
index af574589c1..9c246be4ee 100644
--- a/apps/server/src/mcp/toolkits/pair/handlers.test.ts
+++ b/apps/server/src/mcp/toolkits/pair/handlers.test.ts
@@ -392,6 +392,25 @@ const tagOf = (effect: Effect.Effect {
+ it.effect("lets a paired lead brief, await and stop with Pylon delegation off", () =>
+ Effect.gen(function* () {
+ // A session is paired because the user switched the pair on for this thread.
+ // Only an agent starting a pair by itself still needs the delegation setting.
+ const harness = yield* makeHarness({ shells: [makeShell(LEAD_ID), makeExecutor()] });
+ const paired = invocation({ capabilities: ["pair", "pull-requests"] });
+ expect(
+ yield* harness.call("pair_handoff", { messageKey: "m-1", text: "Step one" }, paired),
+ ).toMatchObject({ accepted: true });
+ expect(yield* harness.call("pair_await", { maxSeconds: 0 }, paired)).toMatchObject({
+ threadId: EXECUTOR_ID,
+ });
+ expect(yield* harness.call("pair_start", {}, paired).pipe(Effect.flip)).toMatchObject({
+ _tag: "McpCapabilityUnavailableError",
+ capability: "delegation",
+ });
+ }),
+ );
+
it.effect("requires the delegation capability for every tool", () =>
Effect.gen(function* () {
const harness = yield* makeHarness({ shells: [makeShell(LEAD_ID), makeExecutor()] });
diff --git a/apps/server/src/mcp/toolkits/pair/handlers.ts b/apps/server/src/mcp/toolkits/pair/handlers.ts
index 7e977e36df..ee6a9d60c4 100644
--- a/apps/server/src/mcp/toolkits/pair/handlers.ts
+++ b/apps/server/src/mcp/toolkits/pair/handlers.ts
@@ -120,6 +120,10 @@ const mapDispatch =
}),
);
+const requirePairCapability = McpInvocationContext.requireMcpCapability("pair").pipe(
+ Effect.catch(() => McpInvocationContext.requireMcpCapability("delegation")),
+);
+
const make = Effect.gen(function* () {
const engine = yield* OrchestrationEngine.OrchestrationEngineService;
const snapshots = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
@@ -343,7 +347,7 @@ const make = Effect.gen(function* () {
readonly steer?: boolean | undefined;
}) =>
Effect.gen(function* () {
- const scope = yield* McpInvocationContext.requireMcpCapability("delegation");
+ const scope = yield* requirePairCapability;
if (!isValidDelegationKey(input.messageKey)) {
return yield* new PairKeyInvalidError();
}
@@ -495,7 +499,7 @@ const make = Effect.gen(function* () {
readonly maxChars?: number | undefined;
}) =>
Effect.gen(function* () {
- const scope = yield* McpInvocationContext.requireMcpCapability("delegation");
+ const scope = yield* requirePairCapability;
const executorId = yield* executorIdFor(scope.threadId);
const existing = yield* findExecutor(executorId);
if (Option.isNone(existing)) {
@@ -601,7 +605,7 @@ const make = Effect.gen(function* () {
const pair_stop = () =>
Effect.gen(function* () {
- const scope = yield* McpInvocationContext.requireMcpCapability("delegation");
+ const scope = yield* requirePairCapability;
const executorId = yield* executorIdFor(scope.threadId);
return yield* withLeadGate(scope.threadId)(
diff --git a/apps/server/src/mcp/toolkits/pair/tools.ts b/apps/server/src/mcp/toolkits/pair/tools.ts
index 768ce2c048..c4695b8b57 100644
--- a/apps/server/src/mcp/toolkits/pair/tools.ts
+++ b/apps/server/src/mcp/toolkits/pair/tools.ts
@@ -2,8 +2,8 @@
* Pair toolkit declaration: one lead thread and one persistent executor thread
* that works in the lead's worktree. The executor's id is the delegated child
* id for the reserved key `pair`, so no contract field or table records the
- * link. Every tool requires the `delegation` capability, which keeps
- * `enableAgentDelegation` as the single kill switch.
+ * link. Starting a pair requires the `delegation` capability; driving an
+ * existing pair accepts either the `pair` or `delegation` capability.
*
* @module mcp/toolkits/pair/tools
*/
diff --git a/apps/server/src/orchestration/DelegationFollowThroughReactor.test.ts b/apps/server/src/orchestration/DelegationFollowThroughReactor.test.ts
index 514b0a0f5a..96a3158181 100644
--- a/apps/server/src/orchestration/DelegationFollowThroughReactor.test.ts
+++ b/apps/server/src/orchestration/DelegationFollowThroughReactor.test.ts
@@ -1,3 +1,4 @@
+import { pairExecutorThreadId } from "@t3tools/shared/delegatedThreads";
import {
DEFAULT_SERVER_SETTINGS,
EventId,
@@ -447,6 +448,28 @@ describe("DelegationFollowThroughReactor", () => {
}),
),
);
+ it.effect("still wakes a lead for its pair executor when delegation is disabled", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const executor = pairExecutorThreadId(PARENT);
+ const h = yield* makeHarness(
+ [shell(PARENT), shell(executor, true), shell(CHILD, true)],
+ false,
+ );
+ // A fan-out child finishing stays silent: that is what the setting is for.
+ h.replace(shell(CHILD));
+ yield* h.emit(CHILD);
+ assert.strictEqual(h.wakes().length, 0);
+ h.replace(shell(executor));
+ yield* h.emit(executor);
+ assert.strictEqual(h.wakes().length, 1);
+ assert.deepStrictEqual(
+ h.wakes()[0]!.children.map((child) => child.threadId),
+ [executor],
+ );
+ }),
+ ),
+ );
it.effect("enforces the persisted three-turn budget without a wake loop", () =>
Effect.scoped(
Effect.gen(function* () {
diff --git a/apps/server/src/orchestration/DelegationFollowThroughReactor.ts b/apps/server/src/orchestration/DelegationFollowThroughReactor.ts
index cd0d27d6ed..1cf3865fb7 100644
--- a/apps/server/src/orchestration/DelegationFollowThroughReactor.ts
+++ b/apps/server/src/orchestration/DelegationFollowThroughReactor.ts
@@ -7,6 +7,7 @@ import {
type OrchestrationEvent,
} from "@t3tools/contracts";
import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
+import { pairExecutorThreadId } from "@t3tools/shared/delegatedThreads";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
@@ -26,6 +27,7 @@ import * as ProjectionSnapshotQuery from "./Services/ProjectionSnapshotQuery.ts"
import {
DELEGATION_OBSERVED_ACTIVITY_KIND,
delegationObservationReceipt,
+ followThroughChildren,
observeDelegatedChild,
isActionableDelegationObservation,
isDelegationParentEligible,
@@ -119,9 +121,13 @@ export const make = Effect.gen(function* () {
yield* settingsService.getSettings,
parent.projectId,
).settings;
- if (!settings.enableAgentDelegation) return;
const snapshot = yield* snapshots.getShellSnapshot();
- const children = snapshot.threads.filter((child) => isChildOfParent(child.id, parent.id));
+ const pairExecutorId = pairExecutorThreadId(parent.id);
+ const children = followThroughChildren({
+ delegationEnabled: settings.enableAgentDelegation,
+ pairExecutorId,
+ children: snapshot.threads.filter((child) => isChildOfParent(child.id, parent.id)),
+ });
if (children.length === 0) return;
const detailOption = yield* snapshots.getThreadDetailById(parent.id, {
activityKinds: [DELEGATION_OBSERVED_ACTIVITY_KIND, DELIVERED, PAUSED],
diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts
index 4a2f5d02b3..14be33b19b 100644
--- a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts
+++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts
@@ -7,7 +7,9 @@ import type {
ThreadId,
} from "@t3tools/contracts";
import { CommandId, OrchestrationCommand } from "@t3tools/contracts";
+import { pairExecutorThreadId } from "@t3tools/shared/delegatedThreads";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
+import { isFollowThroughAdmitted } from "../delegationFollowThrough.logic.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import * as Cause from "effect/Cause";
import * as Clock from "effect/Clock";
@@ -413,8 +415,12 @@ const makeOrchestrationEngine = Effect.gen(function* () {
return {
enabled:
parent !== undefined &&
- resolveProjectSettings(settings, parent.projectId).settings
- .enableAgentDelegation,
+ isFollowThroughAdmitted({
+ delegationEnabled: resolveProjectSettings(settings, parent.projectId).settings
+ .enableAgentDelegation,
+ pairExecutorId: pairExecutorThreadId(command.threadId),
+ childThreadIds: command.children.map((c) => c.threadId),
+ }),
messageExists: Option.isSome(message),
deliveredNotificationIds,
};
diff --git a/apps/server/src/orchestration/delegationFollowThrough.logic.test.ts b/apps/server/src/orchestration/delegationFollowThrough.logic.test.ts
index 6131d97c74..cde6879f37 100644
--- a/apps/server/src/orchestration/delegationFollowThrough.logic.test.ts
+++ b/apps/server/src/orchestration/delegationFollowThrough.logic.test.ts
@@ -11,6 +11,8 @@ import { deriveDelegatedThreadState } from "../mcp/toolkits/delegation/logic.ts"
import {
DELEGATION_OBSERVED_ACTIVITY_KIND,
consumedDelegationObservation,
+ followThroughChildren,
+ isFollowThroughAdmitted,
delegationObservationReceipt,
observeDelegatedChild,
isActionableDelegationObservation,
@@ -256,3 +258,45 @@ describe("an observation a parent read in-turn", () => {
});
});
});
+
+describe("who may wake a parent when Pylon delegation is off", () => {
+ const executor = { id: "delegated:parent:pairpairpairpair" };
+ const fanOut = { id: "delegated:parent:0123456789abcdef" };
+
+ it("keeps every child while delegation is on, and only the pair executor while it is off", () => {
+ const children = [executor, fanOut];
+ expect(
+ followThroughChildren({ delegationEnabled: true, pairExecutorId: executor.id, children }),
+ ).toEqual(children);
+ expect(
+ followThroughChildren({ delegationEnabled: false, pairExecutorId: executor.id, children }),
+ ).toEqual([executor]);
+ expect(
+ followThroughChildren({
+ delegationEnabled: false,
+ pairExecutorId: executor.id,
+ children: [fanOut],
+ }),
+ ).toEqual([]);
+ });
+
+ it("admits a wake with delegation off only when every child named is the pair executor", () => {
+ const base = { pairExecutorId: executor.id };
+ expect(
+ isFollowThroughAdmitted({ ...base, delegationEnabled: true, childThreadIds: [fanOut.id] }),
+ ).toBe(true);
+ expect(
+ isFollowThroughAdmitted({ ...base, delegationEnabled: false, childThreadIds: [executor.id] }),
+ ).toBe(true);
+ expect(
+ isFollowThroughAdmitted({
+ ...base,
+ delegationEnabled: false,
+ childThreadIds: [executor.id, fanOut.id],
+ }),
+ ).toBe(false);
+ expect(isFollowThroughAdmitted({ ...base, delegationEnabled: false, childThreadIds: [] })).toBe(
+ false,
+ );
+ });
+});
diff --git a/apps/server/src/orchestration/delegationFollowThrough.logic.ts b/apps/server/src/orchestration/delegationFollowThrough.logic.ts
index 7009487084..f3cf918de8 100644
--- a/apps/server/src/orchestration/delegationFollowThrough.logic.ts
+++ b/apps/server/src/orchestration/delegationFollowThrough.logic.ts
@@ -68,6 +68,37 @@ export function isActionableDelegationObservation(observation: DelegationObserva
return observation.phase !== "running";
}
+/**
+ * The children whose updates may wake this parent. The Pylon delegation setting
+ * covers what an agent starts on its own. A pair is switched on per thread by
+ * the user, so its executor wakes the lead whether or not that setting is on.
+ */
+export function followThroughChildren(input: {
+ readonly delegationEnabled: boolean;
+ readonly pairExecutorId: string;
+ readonly children: ReadonlyArray;
+}): ReadonlyArray {
+ if (input.delegationEnabled) {
+ return input.children;
+ }
+ return input.children.filter((child) => child.id === input.pairExecutorId);
+}
+
+/** Whether a follow-through wake naming these children may be admitted. */
+export function isFollowThroughAdmitted(input: {
+ readonly delegationEnabled: boolean;
+ readonly pairExecutorId: string;
+ readonly childThreadIds: ReadonlyArray;
+}): boolean {
+ if (input.delegationEnabled) {
+ return true;
+ }
+ return (
+ input.childThreadIds.length > 0 &&
+ input.childThreadIds.every((id) => id === input.pairExecutorId)
+ );
+}
+
/** The activity kind that records what the follow-through reactor last saw of a child. */
export const DELEGATION_OBSERVED_ACTIVITY_KIND = "delegation.child-state";
diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts
index 8358f10fe1..5c0994d353 100644
--- a/apps/server/src/provider/Layers/ProviderService.test.ts
+++ b/apps/server/src/provider/Layers/ProviderService.test.ts
@@ -7112,7 +7112,7 @@ describe("agent browser access", () => {
}).pipe(Effect.provide(NodeServices.layer)),
);
- it.effect("marks a session as paired only when delegation is on and its executor exists", () =>
+ it.effect("marks a session as paired whenever its executor exists and is not archived", () =>
Effect.gen(function* () {
const lead = asThreadId("thread-pair-lead");
const executorOf = (threadId: ThreadId) =>
@@ -7127,7 +7127,18 @@ describe("agent browser access", () => {
["paired", lead, true, [executorOf(lead)], ["delegation", "pair", "pull-requests"], false],
["no executor", lead, true, [], ["delegation", "pull-requests"], false],
["only a fan-out child", lead, true, [otherChild], ["delegation", "pull-requests"], false],
- ["delegation off", lead, false, [executorOf(lead)], ["pull-requests"], false],
+ // The user switched this pair on for this thread, so it does not need the
+ // setting that lets agents start threads on their own.
+ ["delegation off", lead, false, [executorOf(lead)], ["pair", "pull-requests"], false],
+ ["delegation off, no executor", lead, false, [], ["pull-requests"], false],
+ [
+ "delegation off, pair turned off",
+ lead,
+ false,
+ [executorOf(lead)],
+ ["pull-requests"],
+ true,
+ ],
["the executor itself", executorOf(lead), true, [], ["pull-requests"], false],
// Turning a pair off archives an executor that has history. The lead
// must get its own subagents back, not stay in paired mode.
diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts
index 46d9486194..468f42d46f 100644
--- a/apps/server/src/provider/Layers/ProviderService.ts
+++ b/apps/server/src/provider/Layers/ProviderService.ts
@@ -1217,18 +1217,18 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
// A delegated child never receives delegation; its id carries the prefix.
if (access.delegation && !threadId.startsWith("delegated:")) {
capabilities.add("delegation");
- if (Option.isSome(projectionQuery)) {
- const executorId = pairExecutorThreadId(threadId, (input) =>
- NodeCrypto.createHash("sha256").update(input).digest("hex"),
- );
- const executor = yield* projectionQuery.value
- .getThreadShellById(executorId)
- .pipe(Effect.orElseSucceed(() => Option.none()));
- // An archived executor is a pair that was turned off; the lead gets its
- // own subagents and the delegation instructions back.
- if (Option.isSome(executor) && executor.value.archivedAt === null) {
- capabilities.add("pair");
- }
+ }
+ if (!threadId.startsWith("delegated:") && Option.isSome(projectionQuery)) {
+ const executorId = pairExecutorThreadId(threadId, (input) =>
+ NodeCrypto.createHash("sha256").update(input).digest("hex"),
+ );
+ const executor = yield* projectionQuery.value
+ .getThreadShellById(executorId)
+ .pipe(Effect.orElseSucceed(() => Option.none()));
+ // An archived executor is a pair that was turned off; the lead gets its
+ // own subagents and the delegation instructions back.
+ if (Option.isSome(executor) && executor.value.archivedAt === null) {
+ capabilities.add("pair");
}
}
return capabilities;
diff --git a/apps/web/src/components/chat/pairControl.logic.test.ts b/apps/web/src/components/chat/pairControl.logic.test.ts
index c6eb3197d8..519c3a7a0c 100644
--- a/apps/web/src/components/chat/pairControl.logic.test.ts
+++ b/apps/web/src/components/chat/pairControl.logic.test.ts
@@ -5,7 +5,6 @@ import { describe, expect, it } from "vite-plus/test";
import {
pairLockedReason,
- pairSetupReason,
pairToggleStep,
resolveExecutorSelection,
shouldRestartLeadSession,
@@ -38,11 +37,7 @@ const on = (phase: Extract["phase"]): PairState => ({
modelSelection: SELECTION,
activity: null,
});
-const base = {
- executorSelection: SELECTION,
- childRuntimeMode: "inherit" as const,
- delegationEnabled: true,
-};
+const base = { executorSelection: SELECTION, childRuntimeMode: "inherit" as const };
describe("pairLockedReason", () => {
it("makes a change wait while the lead is mid-turn, and only then", () => {
@@ -56,32 +51,7 @@ describe("pairLockedReason", () => {
});
});
-describe("pairSetupReason", () => {
- it("asks for Pylon delegation before a pair can start, and never blocks turning one off", () => {
- expect(pairSetupReason({ delegationEnabled: false, state: off })).toBe(
- "Turn on Pylon delegation in Settings → Integrations to pair.",
- );
- expect(pairSetupReason({ delegationEnabled: true, state: off })).toBeNull();
- expect(pairSetupReason({ delegationEnabled: false, state: on("idle") })).toBeNull();
- });
-});
-
describe("pairToggleStep", () => {
- it("does not start a pair the server would not honor", () => {
- expect(
- pairToggleStep({ ...base, delegationEnabled: false, on: true, state: off, lead: lead() }),
- ).toBeNull();
- expect(
- pairToggleStep({
- ...base,
- delegationEnabled: false,
- on: false,
- state: on("idle"),
- lead: lead(),
- }),
- ).toEqual({ kind: "delete", threadId: EXECUTOR });
- });
-
it("creates the executor in the lead's location when turned on", () => {
expect(pairToggleStep({ ...base, on: true, state: off, lead: lead() })).toEqual({
kind: "create",
diff --git a/apps/web/src/components/chat/pairControl.logic.ts b/apps/web/src/components/chat/pairControl.logic.ts
index ee9e8c750a..c8f55abfe8 100644
--- a/apps/web/src/components/chat/pairControl.logic.ts
+++ b/apps/web/src/components/chat/pairControl.logic.ts
@@ -42,22 +42,6 @@ export function pairLockedReason(lead: PairLead | null): string | null {
return null;
}
-/**
- * What has to be set up before a pair can start, or null when nothing does. The
- * server only gives a lead its pair tools while Pylon delegation is on, so a pair
- * started without it would create an executor nothing could brief. Turning a pair
- * off is never blocked by this.
- */
-export function pairSetupReason(input: {
- readonly delegationEnabled: boolean;
- readonly state: PairState;
-}): string | null {
- if (input.state.kind === "off" && !input.delegationEnabled) {
- return "Turn on Pylon delegation in Settings → Integrations to pair.";
- }
- return null;
-}
-
/** The step a toggle performs, or null when the toggle changes nothing or is not allowed. */
export function pairToggleStep(input: {
readonly on: boolean;
@@ -65,7 +49,6 @@ export function pairToggleStep(input: {
readonly lead: PairLead | null;
readonly executorSelection: ModelSelection | null;
readonly childRuntimeMode: "inherit" | "approval-required";
- readonly delegationEnabled: boolean;
}): PairToggleStep | null {
if (input.lead === null) {
return null;
@@ -76,9 +59,6 @@ export function pairToggleStep(input: {
if (input.state.kind === "unsupported-lead") {
return null;
}
- if (input.on && pairSetupReason(input) !== null) {
- return null;
- }
if (input.on && input.state.kind === "on") {
return null;
}
diff --git a/apps/web/src/components/chat/usePairControl.ts b/apps/web/src/components/chat/usePairControl.ts
index 8e1f7192ef..cd34f437b9 100644
--- a/apps/web/src/components/chat/usePairControl.ts
+++ b/apps/web/src/components/chat/usePairControl.ts
@@ -22,7 +22,6 @@ import { stackedThreadToast, toastManager } from "../ui/toast";
import type { PairControlProps } from "./PairControl";
import {
pairLockedReason,
- pairSetupReason,
pairToggleStep,
resolveExecutorSelection,
shouldRestartLeadSession,
@@ -46,7 +45,6 @@ export function usePairControl(input: {
);
const defaultSelection = projectSettings.delegationDefaultModelSelection;
const childRuntimeMode = projectSettings.delegationChildRuntimeMode;
- const delegationEnabled = projectSettings.enableAgentDelegation;
// Keyed by the lead so a model picked for one thread never follows the user
// to another, without an effect to reset it.
@@ -93,7 +91,6 @@ export function usePairControl(input: {
lead: input.lead,
executorSelection,
childRuntimeMode,
- delegationEnabled,
});
if (step === null) {
return;
@@ -161,7 +158,6 @@ export function usePairControl(input: {
childRuntimeMode,
createThread,
defaultSelection,
- delegationEnabled,
deleteThread,
input.environmentId,
input.lead,
@@ -183,10 +179,7 @@ export function usePairControl(input: {
() => resolveExecutorSelection({ state, picked, defaultSelection }),
[defaultSelection, picked, state],
);
- const lockedReason = useMemo(
- () => pairLockedReason(input.lead) ?? pairSetupReason({ delegationEnabled, state }),
- [delegationEnabled, input.lead, state],
- );
+ const lockedReason = useMemo(() => pairLockedReason(input.lead), [input.lead]);
return useMemo(
() => ({
diff --git a/docs/internals/delegation.md b/docs/internals/delegation.md
index d13ce3a9db..449b49ecc3 100644
--- a/docs/internals/delegation.md
+++ b/docs/internals/delegation.md
@@ -111,8 +111,13 @@ an explicit 0 waits the whole cap; the call still returns the moment the executo
0 reads the state without waiting.
Pairing is session-scoped. When a provider session is prepared, `ProviderService` adds a `pair`
-capability if delegation is on and the thread's executor exists and is not archived. No tool requires
-that capability; adapters read it. A paired Claude query denies the Agent tool (named Task in older
+capability if the thread's executor exists and is not archived. The `enableAgentDelegation` setting
+is not consulted: it covers what an agent starts on its own, and a pair is switched on by the user
+for one thread. So `pair_start` still requires the `delegation` capability, while `pair_handoff`,
+`pair_await` and `pair_stop` accept either one, and the follow-through reactor and the engine's
+admission of its wake let a pair executor through with the setting off (`followThroughChildren`,
+`isFollowThroughAdmitted`). Fan-out children stay silent in that case. Adapters read the `pair`
+capability too. A paired Claude query denies the Agent tool (named Task in older
Claude Code), a paired Codex app-server starts with `features.multi_agent=false` and
`features.multi_agent_v2=false`, and every harness that receives Pylon instructions gets the pair
protocol in place of the delegation block. Nothing is
@@ -130,7 +135,9 @@ tools are deferred behind tool discovery, so a Codex lead that is not told about
spawns its own subagent on its own model instead. That happened in every run until Pylon's
instructions reached Codex (see the Codex protocol traps in [providers](providers.md)). With them, a
fresh Codex lead made one `pair_handoff` and one `pair_await`, a Claude executor did the work, and no
-collaboration tool was called. A Claude lead was verified the same way.
+collaboration tool was called. Claude and Prime Agent leads were verified the same way, Prime on its
+native daemon with an OpenAI Codex model; its log does not name tools, so the evidence is the
+`pair-message:` brief on the executor and the consumed receipt on the lead.
The executor follows its lead. `PairLifecycleReactor` watches domain events and dispatches existing
commands: archiving, settling, or deleting a lead does the same to its executor, including an
diff --git a/docs/user/agent-delegation.md b/docs/user/agent-delegation.md
index cca46abe7c..7a0e2111ae 100644
--- a/docs/user/agent-delegation.md
+++ b/docs/user/agent-delegation.md
@@ -139,8 +139,8 @@ follows **Child permissions**. While the pair is on, the control shows the execu
it is doing, and **Open executor** takes you to its thread. The switch waits while the lead is
mid-turn, because a change applies between turns. Turning the pair off deletes an executor that was
never briefed and archives one that has history; turning it on again brings an archived executor
-back. To change the executor's model, turn the pair off first. You can also ask for a pair in your
-message, for example “Pair with Antigravity for this.”
+back. To change the executor's model, turn the pair off first. With **Pylon delegation** on, you can
+also ask for a pair in your message, for example “Pair with Antigravity for this.”
While a thread is paired, the lead's own subagents are paused for that thread only, so
implementation goes to the executor. Your provider's settings are not changed and other threads are
@@ -151,9 +151,13 @@ cannot be switched off, so it is told to leave them alone rather than prevented
The executor appears under its parent in the sidebar like any delegated thread. Archive it to turn
the pair off for that thread. Archiving, settling, or deleting the lead does the same to its
-executor. Rewinding the lead stops the executor first, because both work on the same files. Pairing
-needs **Pylon delegation** turned on in **Settings → Integrations**; until it is, the switch stays
-off and says so. The Pair control is on the web and desktop apps; the mobile app does not have it yet.
+executor. Rewinding the lead stops the executor first, because both work on the same files. The Pair
+control is on the web and desktop apps; the mobile app does not have it yet.
+
+Pairing does not need **Pylon delegation** turned on. That setting decides whether agents may start
+other threads on their own; a pair is something you switch on yourself, for one thread. With the
+setting off, a paired lead still briefs its executor and is still told when it finishes, and an agent
+that is merely asked to "pair with" another provider cannot start one by itself.
## Things to know