10 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 337 | Dissolve ExtensionRuntime; one source of truth for session state |
Retro: #337 — Dissolve ExtensionRuntime; one source of truth for session state
Stage: Planning (2026-06-06T22:30:24Z)
Session summary
Produced the implementation plan for Phase 4 Step 4: dissolving the ExtensionRuntime god object and unifying session state.
Confirmed Steps 1–3 (#334/#335/#336) are complete and that the transitional RuntimeContextRef seam was explicitly left for this step to retire.
Traced the split-brain through index.ts, runtime.ts, config-store.ts, permission-session.ts, permission-event-rpc.ts, config-modal.ts, and permissions-service.ts.
Observations
- The codebase actually has two split-brains, not one.
The documented one is
PermissionManager/SessionRules: the gate path uses a separatesessionManagerandPermissionSession's privatenew SessionRules(), while the RPC check / config-modal /LocalPermissionsServicereadruntime.permissionManager/runtime.sessionRules. The second, quieter one is context:ConfigStorereads/writesruntime.runtimeContextviaRuntimeContextRef, whilePermissionSessionowns its own privatethis.context— kept in sync only bysession_startcall order. The plan closes both. - Decided to split delivery into a
fix:commit (share single instances, minimal structural change, runtime object still present) and arefactor:commit (dissolve the runtime, retireRuntimeContextRef, deleteruntime.ts). This isolates a realfix:(patch release) from behavior-preserving churn and keeps each commit green. test/runtime.test.tsis fully redundant: every path-derivation case already exists intest/extension-paths.test.tsagainstcomputeExtensionPaths; default-config inconfig-store.test.ts; logger wiring incomposition-root.test.ts. Deletes cleanly with no coverage loss.makeSessioninhandler-fixtures.tsis a duck-typed mock, not a realPermissionSession, so the new injectedSessionRulesconstructor slot only affectspermission-session.test.tscreateSessionandindex.ts— not the gate-handler fixtures.src/runtime.tscan be deleted outright rather than left as a re-export shell: no module importsExtensionPathsfrom it (consumers already import fromextension-paths.ts).- Characterization-test approach: drive a gate session-approval through the composition root with a UI
ctxwhoseui.selectreturnsoptions[1](label-agnostic "for this session"), then assert the RPC check andgetPermissionsService().checkPermissionboth reportallow. Red on current code (RPC reads empty session rules), green after the fix. - No
ask_userneeded — the issue's proposed change and the roadmap pin the design unambiguously.
Stage: Implementation — TDD (2026-06-06T23:03:30Z)
Session summary
Completed all three TDD steps: a fix: commit sharing one PermissionManager and SessionRules across the gate and RPC paths (bug fix), a refactor: commit dissolving ExtensionRuntime and retiring RuntimeContextRef, and a docs: commit updating the architecture roadmap plus marking Step 4 complete.
Test count: 1837 → 1838 (characterization test added in composition-root.test.ts) → 1815 (86 files; runtime.test.ts deleted, its 23 tests already covered by extension-paths.test.ts).
Pre-completion reviewer returned WARN; both findings were resolved inline.
Observations
- The
prefer-const/@typescript-eslint/no-unnecessary-conditionlint conflict on thesessionforward reference was resolved by introducing asessionNotify: PermissionSession | null = nullholder. The logger's notify closure usessessionNotify?.getRuntimeContext()(correctly nullable), whilesessionis declaredconstafterPermissionSessionis constructed. This is cleaner thannull as unknown as PermissionSessionbecause optional chaining on the holder is safe, and there is noas unknown ascast to suppress. - The
let configStore = null as unknown as ConfigStoreforward reference mirrors the pattern that existed increateExtensionRuntimeand was retained (documented in a comment). - Two context-seam tests in
config-store.test.ts("updates context via context.set", "does not overwrite context when ctx is omitted") were replaced with direct ctx-parameter behavior tests ("uses the passed ctx cwd forloadAndMergeConfigs", "uses empty string cwd when no ctx is provided"). No coverage lost; themakeContextRefhelper andRuntimeContextRefimport were removed. - The
@typescript-eslint/no-deprecatedlint error onPERMISSIONS_RPC_CHECK_CHANNELin the new composition-root test was fixed by extracting the channel value to a localconst rpcCheckChannel: string = PERMISSIONS_RPC_CHECK_CHANNELwith a singleeslint-disable-next-lineannotation — cleaner than per-use suppressions. - Reviewer WARN 1 (Step 4 not marked
✓ completeinarchitecture.md) was addressed immediately with an additionaldocs:commit, per the package skill requirement to mark steps complete at ship time rather than deferring. - Reviewer WARN 2 (pre-existing three-field cache reset in
permission-session.tswithout aclearCaches()helper) is a known smell documented in the Phase 4 plans; not introduced by this PR.
Stage: Final Retrospective (2026-06-06T23:33:45Z)
Session summary
Shipped Phase 4 Step 4 end-to-end across four stages (plan → TDD → ship → retro): dissolved the ExtensionRuntime god object, fixed the session-rules / PermissionManager split-brain, and released pi-permission-system@v10.3.1.
The plan was accurate enough that TDD execution matched it almost line-for-line; all friction was lint-driven micro-rework caught pre-commit by the verification loop.
No user corrections were needed beyond a single "Continue." nudge and a status check.
Observations
What went well
- Model-task fit across the workflow was clean (see Diagnostic details):
opusfor plan/retro judgment,sonnetfor implementation, and adeepseek-v4-flashmodel executing the deterministic/ship-issueworkflow flawlessly — correctly escalating the one judgment point (stacked-release batch-vs-now) to the user viaask_userrather than deciding alone. - The
sessionNotify: PermissionSession | nullnullable-holder pattern cleanly resolved the forward-reference + Biome/ESLint conflict inindex.tswithout anas unknown ascast on the session reference (theconfigStoreref kept the pre-existingnull as unknown as ConfigStoreidiom). - Plan accuracy: the two-split-brain diagnosis, the
fix:+refactor:split, and the zero-coverage-loss deletion ofruntime.test.tsall played out exactly as the plan predicted — the planning-stage exploration (reading every consumer +extension-paths.test.tsoverlap) paid off directly. - The verification loop ran incrementally (
pnpm run checkafter each interface change, targetedvitestfor red/green, full lint+test before every commit), so every slip was caught before commit.
What caused friction (agent side)
instruction-violation(self-identified) — the firstindex.tsrewrite usedconfigStore!.current()(a!assertion) plus aneslint-disablefor@typescript-eslint/no-non-null-assertion, despite AGENTS.md's "Biome / ESLint linter conflicts" rule banningx!.pnpm run lintflagged it (5 errors: 2×prefer-const, the unused disable directive, 2× unnecessary optional chain); the fix was the nullable-holder rewrite. Impact: oneindex.tsrewrite, caught pre-commit — no wasted commit, no shipped defect.other(self-identified) — appending the TDD stage entry to the retro viaEditproduced a malformed file: the suppliednewTextwas incomplete (ended mid-sentence) and the one-sentence-per-line autoformat interleaved it into the Planning observations. Re-reading caught it; a fullWritefixed it. Impact: one retro rewrite. Note: retro files accumulate repeated### Observations/### Session summaryheaders across stages, soEditanchors on those headers are inherently ambiguous.other(self-identified) — two transient editing slips inconfig-store.ts/ the deprecated-channel test: a deadcwdOrNulllocal (removed immediately) andeslint-disablecomments placed on the wrong lines (refactored to a single local-const disable). Impact: ~1 extra edit each; no rework beyond the same step.
What caused friction (user side)
- None of substance. The user's involvement was light-touch oversight (one "Continue." after a turn ended without a trailing tool call, and one "Where are we?" status check) rather than strategic correction — appropriate for a session executing a detailed pre-approved plan.
Diagnostic details
- Model-performance correlation — Planning
anthropic/claude-opus-4-8(judgment-heavy design tracing), TDDanthropic/claude-sonnet-4-6(implementation), Shippingopencode-go/deepseek-v4-flash(mechanical procedural workflow), Retroanthropic/claude-opus-4-8(synthesis). No mismatches: the flash model on/ship-issueis the intended fit — a deterministic workflow where the sole judgment call was correctly delegated to the user. Thepre-completion-reviewersubagent ran on its own frontmatter model and produced a thorough WARN report (deterministic checks, acceptance criteria, code design, Mermaid parse) — quality adequate for the judgment-heavy review. - Escalation-delay tracking — no rabbit-holes; the longest same-error sequence was the
index.tslint conflict at one lint run → one rewrite (well under the 5-call threshold). - Unused-tool detection — no
missing-contextorrabbit-holepoints; no subagent orcolgrepopportunity was missed (planning-stagegrep/readexploration was sufficient). - Feedback-loop gap analysis — no gap; verification was incremental throughout, not end-loaded.
Changes made
.pi/prompts/tdd-plan.md— added retro-append guidance: anchor theEditon the file's last line or useWrite, since repeated stage headers make header-anchored edits ambiguous..pi/prompts/build-plan.md— same retro-append guidance line..pi/prompts/retro.md— same guidance, added beside the existing "append the new entry" instruction in Step 3.