Skip to content

v0.7.6

Choose a tag to compare

@junghan0611 junghan0611 released this 27 May 10:56
· 253 commits to main since this release

Async-resume regression repair. Closes the most awkward intermediate state on the entwurf surface — short spawn defaulting async (0.7.0, ad4413e) while long resume blocked the parent turn (Phase 0.5, agent-config e5aa5a1, 2026-04-24) — and restores the pre-Phase-0.5 native pattern across both the native pi tool surface and the MCP bridge surface that pi-shell-acp Claude (and any other replyable pi-session caller) actually uses. Verified live across Claude, Codex, and Gemini (Hard Rule #7), plus a backend-agnostic handler-level proof + the external rejection path.

Changed

  • entwurf_resume native default flipped syncasync (Phase A). Restores the pre-Phase-0.5 behavior where long-running resumes (review / research / build) detach and deliver completion as a followUp message instead of blocking the parent turn. The async branch (pi-extensions/entwurf.ts:929-1137 at the time) was already alive and unchanged — only the schema default and the params.mode ?? "..." runtime fallback were inverted. This completes the 0.7.0 axis symmetry that left resume on the Phase 0.5 sync default — producing the inverted state where short spawn (often <5s) detached while long resume blocked. Sync stays available as mode="sync" opt-in for short status-check resumes (<5s).

  • MCP entwurf_resume exposes mode with conditional default + replyable gate (Phase B Step 3). The user-facing path: pi-shell-acp Claude and any other replyable pi-session caller of the MCP bridge now get async by default; external MCP hosts (Claude Code standalone, Codex CLI, Gemini CLI) stay on sync because they cannot receive followUp delivery. The discriminator is the existing buildSendSenderEnvelope replyable status — the same gate entwurf_send already uses for wants_reply rejection. A static default: "async" would have inverted external-host UX; the conditional default closes that. Explicit mode='async' from an external host is rejected with the canonical ENTWURF_RESUME_ASYNC_REJECT_REASON text, mirroring the wants_reply=true rejection pattern. Implementation note: the MCP handler does NOT clone the async launcher — it delegates back into the parent pi session via a new spawn_async_resume entwurf-control RPC (Step 2), so completion delivery stays in the pi extension layer where it belongs ("this bridge is not a second harness" invariant).

  • entwurf_resume cwd is sync-only at the MCP surface (silent-ignore guard). The async launcher uses the saved session header cwd as authority (#9); the previous (pre-0.7.6) MCP schema accepted cwd but the async path silently dropped it. Now the handler rejects effectiveMode='async' + cwd explicitly with a canonical reject reason so callers do not believe their override took effect when it didn't. The replyable check still fires first when both could apply — callers see the more fundamental wiring break before the cwd detail.

Added

  • pi-extensions/lib/entwurf-async.ts — shared async resume launcher + state (Phase B Step 1). Hoists the async resume body (the 235-line block formerly at pi-extensions/entwurf.ts:929-1137) plus activeEntwurfs, AsyncEntwurfInfo, findEntwurfSession, isProcessAlive, and ENTWURF_ENTRY_TYPE into a single library module. Both the native entwurf_resume tool and the new entwurf-control spawn_async_resume RPC import the same spawnEntwurfResumeAsync(...) launcher and read/write the same activeEntwurfs Map — /entwurf-status sees every async task regardless of which surface spawned it (SSOT). The launcher accepts callbacks (appendActiveEntry / deliverCompletion) rather than depending on ExtensionAPI directly, so the lib stays platform-neutral and each callsite supplies its own parent-session notification surface. shellQuote source parity now spans three sites (added pi-extensions/lib/entwurf-async.ts to check-shell-quote SOURCE_SITES).

  • spawn_async_resume entwurf-control RPC (Phase B Step 2). New command in the entwurf-control dispatcher: { type: "spawn_async_resume", taskId, prompt, host? } → respond with { taskId, originalTaskId, sessionFile, pid, text } on success, propagate the launcher's throws (Identity Preservation Rule, missing session file, missing cwd authority) verbatim as RPC errors. Lets the MCP bridge surface dispatch replyable async resumes by delegating into the parent pi session's extension context instead of cloning the launcher.

  • check-async-resume-gate deterministic gate (Phase B Step 4). New scripts/check-async-resume-gate.ts exercises the conditional-default + cwd-silent-ignore logic — 16 assertions across 6 mode-resolution cases (replyable/external × {omit, sync, async} combinations) + 7 invariants (canonical reject text shape, no-silent-downgrade regression guard, defensive replyable: undefined handling, cwd guard ordering). Resolution logic lives in mcp/pi-tools-bridge/src/resume-mode.ts so the gate can import it without triggering the MCP server's main() side effect. Wired into pnpm check between check-plugin-prompt-format and check-models. No spawn, no socket, no API cost.

  • smoke-async-resume live gate (Phase B Step 5, three-backend axis). New scripts/smoke-async-resume.sh (432 lines, pattern from session-messaging-smoke.sh). Six cases:

    • A.async.{claude,codex,gemini} — disposable tmux pi session per backend, prompt the backend procedurally (no identity claims) to chain MCP entwurf sync-only spawn → entwurf_resume(mode='async'), assert "Resume spawned (async)" ack AND "🏁 resume … completed" followUp in the pane. This proves the replyable MCP → control-RPC → native async launcher path on all three backends. The omitted-mode conditional default itself is pinned by check-async-resume-gate; the live gate uses explicit mode='async' so model prompt-following cannot hide the async branch.
    • D.direct_stdio_async_handler — handler-level proof independent of any backend's prompt-following: sets PI_SESSION_ID/PI_AGENT_ID env directly, stdio-calls the MCP bridge to spawn an entwurf and then entwurf_resume(mode='async'). Asserts the async ack text returns. Separates "handler correctness" from "backend prompt-following capability."
    • B.external_async_reject — external (no PI_SESSION_ID) + explicit mode='async' → reject with canonical text.
    • C.external_autosync_shape — external + mode omitted → auto-sync path reached (asserts via session_not_found on a synthetic taskId).

    Reference baseline run (2026-05-27, artifact /tmp/smoke-async-resume-20260527-194013.json): 6 PASS / 0 FAIL / 0 SKIP, three-backend equality closed, strict fail-closed (❌ resume is FAIL, only 🏁 resume … completed is PASS). The final smoke also captured a useful boundary lesson: user-role identity assertions like "you ARE replyable" are rightly rejected by Claude as prompt-injection-shaped; the smoke prompt is therefore procedural and lets the MCP env decide replyability. Cost for the final full run was ~$0.11 (overall Phase B evidence formation ~$0.55). Required before every release that touches the entwurf surface; not in the deterministic pnpm check chain because it spends ACP turns.

Verification

  • pnpm check — 13 deterministic gates pass (check-mcp, check-shell-quote 17 (3 source-parity sites + reference body + 13 behavior cases), check-plugin-empty-final-recovery 34, check-plugin-prompt-format 22, check-async-resume-gate 16, check-models 3 passes, check-backends 136, check-registration 8, check-dep-versions 6, check-sdk-surface 0 unannotated casts, check-pack 52-file invariant — was 48 in 0.7.5, +4 for pi-extensions/lib/entwurf-async.ts, mcp/pi-tools-bridge/src/resume-mode.ts, scripts/check-async-resume-gate.ts, scripts/smoke-async-resume.sh).
  • ./run.sh smoke-async-resume — 6 PASS / 0 FAIL / 0 SKIP across Claude + Codex + Gemini (final strict fail-closed baseline reference run above).
  • Three-backend equality (Hard Rule #7) — Claude, Codex, Gemini all GREEN with live evidence.

Repository

  • Commit chain (main): ff85fa9 Phase A native default flip → 4b89b81 Step 1 lib extraction → 0107ce4 Step 2 control RPC → 684c97b Step 3 MCP mode + conditional default → 69ff04b Step 4 deterministic gate → b28d1bb cwd silent-ignore fix → b6ef765 Step 5 live smoke → 24ee129 0.7.6 release docs/version → b98774b pre-release surface-doc alignment + fail-closed smoke → d198da0 procedural smoke prompt.
  • AGENTS.md, README, VERIFY.md, NEXT.md, and MCP/source comments updated to the final 0.7.6 surface: MCP entwurf spawn remains sync-only; MCP entwurf_resume is conditional-default async for replyable pi-session callers and sync/reject for external non-replyable hosts.
  • NEXT.md — async-resume repair moves out of active state. Phase A and Phase B complete; next focus returns to Asymmetric Mitsein / session-continuity hygiene.