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
6 changes: 6 additions & 0 deletions docs/gentle-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,12 @@ A finished list stays on screen for the turn it finished in and clears at the ne

### Bridge providers

Idle Claude Bridge continuation uses the normal user-prompt lifecycle so prompt preparation and tool declarations are retained. Gentle Agents hides only its exact, uniquely generated reserved wake in Pi's interactive transcript; child result and query cards remain visible. Native providers keep their hidden `display: false` custom wake.

The reserved identity is stored once per session as a non-context custom entry and reconstructed on reload/resume, including identities on abandoned branches. It is session-owned: switching sessions releases the previous identity. Ordinary text quoting the old notification, prefixes, substrings, and whitespace variations is not hidden. Pi's transformer exposes no author/message ID, so an exact copy of the current reserved identity is indistinguishable from its generated wake.

This is **TUI-only suppression**: the generated user-role message remains in session history, model context, and RPC. It does not fix the SDK's idle custom-message lifecycle. Older runtimes without the Markdown transformer API, or failed identity persistence, retain visible continuation and emit a warning rather than dropping the wake. Existing unreserved wakes are not retroactively hidden.

The Gentle AI harness (ODD workflow, identity, review contract) and the open-tasks block are appended to `before_agent_start`'s `systemPromptOptions.appendSystemPrompt` instead of being returned as a replacement `systemPrompt` (gentle-shell#1485). Provider bridges such as `pi-claude-bridge` forward only those structured sections after their own preset and drop a returned `systemPrompt`, so this route reaches every provider, bridged or not.

Set `GENTLE_PI_SHELL=0` to keep pi's built-in footer and editor.
Expand Down
92 changes: 79 additions & 13 deletions extensions/gentle-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ const TOOL_PREFIX = "subagent_";
// Wakes an idle parent after child content was stored as a custom message.
// It names itself as automated so the model never attributes it to the human.
const PARENT_WAKE_TEXT = "[System-generated Gentle Agents notification, not written by the user] Subagent output was delivered to this session above. Review it and continue.";
const PARENT_WAKE_TYPE = "gentle-agents.wake";
const BRIDGE_WAKE_IDENTITY_TYPE = "gentle-agents.wake-identity";
interface BridgeWakeIdentity {
sessionId: string;
nonce: string;
text: string;
}
const bridgeWakeText = (nonce: string): string => `${PARENT_WAKE_TEXT} [gentle-agents wake: ${nonce}]`;
const NATIVE_PARENT_WAKE_TEXT = "Review the delivered subagent output and continue.";
// How long a dispatched wake may take to start a parent run before a later
// delivery may send another one.
export const PARENT_WAKE_GRACE_MS = 30_000;
Expand Down Expand Up @@ -595,6 +604,53 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
// session_start context is kept for it and dropped at shutdown; a stale
// context throws instead of answering, so delivery fails closed.
let parentCtx: ExtensionContext | undefined;
let bridgeWakeIdentity: BridgeWakeIdentity | undefined;
let wakeVisibilityWarning = false;
const restoreBridgeWakeIdentity = (ctx: ExtensionContext | undefined) => {
bridgeWakeIdentity = undefined;
if (!ctx) return;
// Rendering is session-wide, not model/branch state: an identity on an
// abandoned branch still owns its generated bubbles in the session tree.
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type !== "custom" || entry.customType !== BRIDGE_WAKE_IDENTITY_TYPE) continue;
const data = entry.data as Partial<BridgeWakeIdentity> | undefined;
if (data?.sessionId === ctx.sessionManager.getSessionId() && typeof data.nonce === "string"
&& /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(data.nonce)
&& data.text === bridgeWakeText(data.nonce)) {
bridgeWakeIdentity = data as BridgeWakeIdentity;
break;
}
}
};
const hasWakeTransformer = typeof pi.registerMarkdownTransformer === "function";
if (hasWakeTransformer) {
// Register once in this runtime. Reload replaces the runtime; session
// replacement changes the identity, never the transformer registration.
pi.registerMarkdownTransformer((markdown, context) =>
context.messageType === "user" && bridgeWakeIdentity?.sessionId === parentCtx?.sessionManager.getSessionId()
&& markdown === bridgeWakeIdentity?.text ? "" : markdown);
}
const bridgeWake = (): string => {
if (!parentCtx) return PARENT_WAKE_TEXT;
try {
if (!hasWakeTransformer) throw new Error("Markdown transformer API unavailable");
if (!bridgeWakeIdentity || bridgeWakeIdentity.sessionId !== parentCtx.sessionManager.getSessionId()) {
const nonce = randomUUID();
const identity = { sessionId: parentCtx.sessionManager.getSessionId(), nonce, text: bridgeWakeText(nonce) };
// Persist before sending so a restart can reconstruct exact ownership.
pi.appendEntry(BRIDGE_WAKE_IDENTITY_TYPE, identity);
bridgeWakeIdentity = identity;
}
return bridgeWakeIdentity.text;
} catch {
// Compatibility fallback preserves continuation, not invisibility.
if (!wakeVisibilityWarning) {
wakeVisibilityWarning = true;
try { parentCtx.ui.notify("Gentle Agents cannot hide Claude Bridge continuation on this runtime; the generated user wake remains visible.", "warning"); } catch { /* UI failure must not drop continuation. */ }
}
return PARENT_WAKE_TEXT;
}
};
// Mirrors the host's agent run, which spans agent_start through
// agent_settled, including post-run retries and in-run compaction. Unlike
// activeAgentRuns it stays set between agent_end and agent_settled, where
Expand Down Expand Up @@ -629,13 +685,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
// branch, so a parent that keeps calling tools would see the content only
// when the whole run ends — the original #867 delay.
//
// An idle parent must not get triggerTurn: the host would run the custom
// message as a direct turn that skips the prompt lifecycle
// (before_agent_start and the prompt refresh), and prompt-capture
// integrations such as the Claude bridge reject that turn. The structured
// message is stored durably without a turn instead, and a short
// system-generated user message wakes the parent through the normal prompt
// path. The wake never repeats child content, so the model sees it once.
// Idle child content is stored durably without a turn, then a separate
// coalesced wake requests continuation without repeating that content.
// Claude Bridge needs a user wake through the prompt lifecycle for capture;
// native providers can use a hidden custom-message turn instead.
//
// A parent that is busy without a run (compaction, or a prompt's pre-run
// compaction) is not streaming, so steer + triggerTurn would also start a
Expand Down Expand Up @@ -688,7 +741,18 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
try {
// "steer" matters only when a run started in between: the wake is then
// queued into it instead of being rejected as a concurrent prompt.
pi.sendUserMessage(PARENT_WAKE_TEXT, { deliverAs: "steer" });
// Read the live selection at dispatch, not when child content arrived.
// Only Claude Bridge is currently evidenced to require prompt capture;
// registering a custom provider alone does not make it a bridge.
if (parentCtx?.model?.provider === "claude-bridge") {
pi.sendUserMessage(bridgeWake(), { deliverAs: "steer" });
} else {
pi.sendMessage({
customType: PARENT_WAKE_TYPE,
content: NATIVE_PARENT_WAKE_TEXT,
display: false,
}, { deliverAs: "steer", triggerTurn: true });
}
} catch {
// A stale runtime fails closed instead of throwing from a microtask.
endPromptStart();
Expand Down Expand Up @@ -784,6 +848,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
// Session changes discard every pending wake and boundary flush.
const resetParentDelivery = (ctx: ExtensionContext | undefined) => {
parentCtx = ctx;
restoreBridgeWakeIdentity(ctx);
wakeVisibilityWarning = false;
parentRunActive = false;
wakeOwed = false;
endPromptStart();
Expand Down Expand Up @@ -1065,12 +1131,12 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
} catch {
return;
}
// The session may have moved on while disk was read; a stale restore
// must never land in the wrong session's store.
if (ctx.sessionManager.getSessionId() !== sessionId) return;
// Fire-and-forget from session_start: a throwing summary subscriber must
// never surface as an unhandled rejection. History is best-effort.
// Fire-and-forget from session_start: a stale SDK context or throwing
// summary subscriber must never surface as an unhandled rejection.
try {
// The session may have moved on while disk was read; a stale restore
// must never land in the wrong session's store.
if (ctx.sessionManager.getSessionId() !== sessionId) return;
for (const { task, thread } of history) {
if (task.parentSessionId !== sessionId) continue;
if (store.restore(task, thread)) restoredTaskIds.add(task.id);
Expand Down
35 changes: 35 additions & 0 deletions odd/tasks/bridge-only-agent-wake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Bridge-only agent wake

## Intent
- Objective: show the synthetic Gentle Agents user notification only when the orchestrator's currently selected provider comes from a bridge plugin (for example Claude Bridge).
- Problem: idle background subagent delivery unconditionally emits a visible synthetic user turn.
- Why: native Pi providers should continue automatically without this chat noise.
- Authorized scope: parent wake routing and focused regression tests. Preserve result delivery, coalescing, streaming steering, and prompt-start grace. Do not change installer work or bridge plugins.
- Acceptance: bridge selected => existing user wake; native selected => hidden custom wake that still triggers a turn; runtime provider switching is respected; background completion and query behavior remains correct.

## Plan
- [ ] T1 — Route idle wakes by the live selected provider and prove regressions. Route: delegated (non-trivial extension and test edits, SDK exploration). Risk: medium initially; native assessment will determine independent checks. Test-first: observe focused regression RED, implement GREEN, rerun wake-related tests and typecheck. Keep tests with behavior in one work-unit commit.

## Delivery
- Strategy: ask-on-risk. Forecast: approximately 120 authored additions/deletions, excluding generated output. No publishing or PR authorized.
- Branch: `fix/bridge-only-agent-wake`; unrelated dirty installer files preserved.
- Work-unit commit: pending.
- Running authored count: 0.

## Progress and evidence
- T1: in progress; read-only mapping found unconditional `sendUserMessage` in `extensions/gentle-agents.ts` dispatchWake.
- SDK supports custom hidden messages with `triggerTurn:true`; simply dropping the user message would lose continuation.
- Bridge means selected model provider, not RPC/UI mode or arbitrary extension registration.
- Exact surfaces: `extensions/gentle-agents.ts`, `tests/gentle-agents.test.ts`. Current evidenced bridge ID: `claude-bridge`; no SDK bridge semantic marker. Do not classify arbitrary extension providers as bridges.
- Writer `muqwuqx6-2-bzz2` returned partial: extension and tests changed (144 additions, 16 deletions). RED: 175 pass / 11 intended failures. GREEN: 186 pass / 0 fail. Fourteen deterministic cases added.
- `node --experimental-strip-types --test tests/gentle-agents.test.ts`: passed 186/186 for writer, independent verifier and parent. `pnpm run typecheck`: failed with two TS2345 errors in unchanged `tests/installer-posix-bootstrap.test.ts:235,238` from an untyped heterogeneous tuple loop (:226–231). Statements are identical in HEAD and do not depend on candidate code; clean-HEAD compilation unverified.
- Independent verifier `muqx30qs-3-6rgo`: focused tests passed 186/186. Typecheck failed with the two TS2345 errors above. Full `pnpm test` failed: 4684 tests, 4648 passed, 1 failed, 35 skipped; provider-contract and runtime-harness stages passed. Failure: `tests/gentle-ai-dev-binary-surfacing.test.ts:189` (assertion :202), `session start defers the active-override announcement to the shell card`. Causal inspection completed: inherited `GENTLE_PI_AGENTS_CHILD=1` disables shell in unchanged `lib/shell-bar.ts:109–112`, causing the fallback warning in unchanged `extensions/gentle-ai.ts:9371–9374`. Test clears GENTLE_PI_SHELL but not child flag. No unrelated fixes or reruns performed.
- Parent spot check: focused command passed 186/186 (~20.5 seconds).
- RDD inspect performed; workspace scope mixes installer changes with this candidate and requires intended-untracked selection. No START performed; avoid freezing unrelated work.
- Native ASSESS returned unassessable due to unrelated untracked scope; plan requires independent verifier (treated as high). Isolate this work unit as a committed range before review; do not review accumulated installer work.
- Required checks: focused deterministic tests, related agent delivery tests, typecheck, parent spot check, RDD for this candidate under the user-owned switch.
- Independent verifier final: no candidate-caused blocker found, but overall checks not green. SDK confirms hidden custom wake starts a model turn; tests mock calls rather than run a live model. Writer RED/GREEN observed; parent inspected extension diff and reran focused tests. Native review and commit pending. T1 remains incomplete until delivery/check decisions are resolved.
- [ ] B1 — Resolve verification blockers and isolate review scope. Investigate causality read-only; unrelated fixes require separate authorization. Do not silently accept failed checks.

## Next step
Needs user decision: authorize an isolated work-unit commit to review only this change, with unrelated check failures explicitly retained, or leave implementation uncommitted and pause. No publishing/PR/merge. Do not change installer or shell tests without separate scope authorization.
Loading
Loading