From 862d1b6d921f421ff0eaeb2f1df16274445dbc01 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 25 Aug 2026 08:46:55 +0800 Subject: [PATCH] fix(pi): reopen persisted conversation sessions --- ...25-pi-session-reopen-installer-8c3e7a91.md | 115 ++++++++++++++++++ electron/coding-runtime/pi/runtime.ts | 14 ++- tests/unit/pi-managed-worker-opener.test.ts | 27 +++- 3 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260825-pi-session-reopen-installer-8c3e7a91.md diff --git a/.project-docs/30-worklog/tasks/20260825-pi-session-reopen-installer-8c3e7a91.md b/.project-docs/30-worklog/tasks/20260825-pi-session-reopen-installer-8c3e7a91.md new file mode 100644 index 0000000..0aac366 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260825-pi-session-reopen-installer-8c3e7a91.md @@ -0,0 +1,115 @@ +# Task: Fix persisted Pi session reopen and build Windows installer + +## Identity + +- Task ID: 20260825-pi-session-reopen-installer-8c3e7a91 +- Mode: Feature +- Branch: codex/20260825-pi-session-reopen-installer-8c3e7a91-pi-session-reopen-installer +- Worktree: D:\Datas\OthersProjects\makelore-pi-session-reopen-installer-8c3e7a91 +- Base commit: e74b59a67479a32619a9e3dbef476a65a6ad071f +- Owner: codex-root +- Status: Implementation + +## Scope + +- Fix the packaged Pi runtime path that opens generation 1 without the + Conversation's already persisted Pi session binding after an application + restart or later re-entry. +- Add focused regressions proving the managed opener reuses the Registry + binding when `PiWorkerPool` does not provide an explicit `existingSession`, + while a genuinely new Conversation still creates one binding. +- Build and verify a Windows x64 NSIS installer from the cumulative hotfix base + `e74b59a67479a32619a9e3dbef476a65a6ad071f` plus this repair. + +## Intent And Constraints + +- Preserve the user's existing Pi JSONL and Conversation metadata. Do not + delete, rewrite, migrate, or silently replace a persisted binding. +- Derive the effective reopen binding from the explicit worker-pool binding or + the Main-owned Registry record, in that order. Keep the existing + `CODING_SESSION_UNREADABLE` mismatch checks fail-closed. +- Keep the repair inside the existing Pi managed opener/Registry boundary; do + not add a fallback runtime, compatibility layer, or product migration. +- Preserve new-Conversation allocation and fork semantics, and report a + Registry-backed reopen as warm telemetry. +- The previously diagnosed thinking-level submission stall is a separate task + with no accepted implementation. Do not mix an unverified fix into this + installer. +- Use pinned pnpm `10.33.4`, focused tests, full repository checks, + `build:vite`, Windows Electron E2E, formal Windows packaging, and final + artifact/runtime verification. +- Real Provider verification remains `Explicitly Waived / Accepted Risk` with + `realTurnVerified=false`; loopback/package evidence is not a real Provider + Pass. macOS and native non-WSL Linux evidence status is unchanged. + +## Project Context Loaded + +- Concurrent Task Gate: Passed in the isolated worktree owned by `codex-root`; + the task record, branch, worktree, and base commit match the registry. +- Planning Gate: Passed after reading the project entry/memory indexes, current + state, architecture/data-flow/evidence/commitment records, the installed + session-unreadable diagnosis, the cumulative Windows proxy hotfix, the + thinking-stall diagnosis, PI-150 packaging evidence, and all peer records + still marked planning or blocked. +- Canonical current-state documents predate the cumulative Pi delivery. The + current source, diagnostic evidence, hotfix task record, and cumulative + release-proof record are authoritative for this repair. +- Other planning tasks are on older or unrelated AI Design/Robot/OpenCode + baselines. The main-worktree OpenCode integration task is blocked and does + not package; no unresolved semantic conflict or code-write overlap changes + this plan. +- No subagents are used, per user direction and repository policy. + +## Plan + +1. Add a focused red regression for a Registry-persisted binding with no + explicit `existingSession`, including session-id argv and warm telemetry. +2. Make the managed opener use one effective binding consistently for session + selection, fork rejection, reopen mismatch validation, and telemetry. +3. Run focused Pi tests, typecheck, lint, full unit tests, `build:vite`, and + Windows Electron E2E; fix only task-caused failures. +4. Commit the clean candidate, build the Windows NSIS, and run final Windows + artifact/Pi/runtime/package smoke checks, including persisted-session reopen + and fresh-Conversation binding behavior. +5. Record the final installer path, size, SHA-256, exact evidence, and remaining + accepted/deferred risks; complete the task documentation gate. + +## Outcome + +- Implemented one effective reopen binding in `createPiManagedWorkerOpener`: + explicit worker-pool binding first, otherwise the persisted Registry binding. + Session-key selection, reopen/fork validation, Pi session-id mismatch checks, + and cold/warm telemetry now use that same binding. +- Preserved the existing post-open Registry mismatch guard and session-file + containment checks. No user session, Conversation schema, or migration path + changed. +- Extended the managed opener regression to simulate a process restart with a + fresh `PiSessionRegistry`: the persisted binding is reused without invoking + the new-session key generator, while the original first-open path still + creates exactly one binding. +- Windows packaging and final artifact verification remain in progress. + +## Verification + +- Frozen install with pinned pnpm `10.33.4`: passed; lockfile unchanged. +- Red regression before implementation: failed at the exact existing + `Pi session binding does not match the Conversation registry` branch. +- Focused managed opener/Registry/runtime/worker-pool suite: 4 files / 23 tests + passed after the repair. +- `corepack pnpm run typecheck`: passed. +- `corepack pnpm run lint:check`: passed with 0 errors and five pre-existing + Renderer warnings. +- `corepack pnpm test`: passed; 178 primary files / 1512 passed / 2 skipped, + plus the isolated REN-008 pressure test 1/1. +- `corepack pnpm run build:vite`: passed with only existing advisory warnings. +- `corepack pnpm run test:electron:windows`: 2 files / 4 tests passed. +- `git diff --check`, task ownership drift, and required project-doc checks: + passed before packaging. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- None recorded. diff --git a/electron/coding-runtime/pi/runtime.ts b/electron/coding-runtime/pi/runtime.ts index 4eb1301..fd33150 100644 --- a/electron/coding-runtime/pi/runtime.ts +++ b/electron/coding-runtime/pi/runtime.ts @@ -191,6 +191,7 @@ export function createPiManagedWorkerOpener( return async (input) => { const resourcesStartedAt = now(); const registered = await options.registry.prepare(input.conversation); + const existingSession = input.existingSession ?? registered.session ?? undefined; const model = registered.conversation.model; if (!model || registered.conversation.modelResolution !== 'resolved') { throw new CodingRuntimeContractError( @@ -238,11 +239,12 @@ export function createPiManagedWorkerOpener( 'resources.ready', now() - resourcesStartedAt, now(), + !existingSession, ); const sessionKey = validateSessionKey( - input.existingSession?.sessionKey ?? createSessionKey(), + existingSession?.sessionKey ?? createSessionKey(), ); - if (input.existingSession && input.fork) { + if (existingSession && input.fork) { throw new Error('Pi worker cannot reopen and fork a session at the same time'); } const process = createProcess({ @@ -272,6 +274,7 @@ export function createPiManagedWorkerOpener( 'worker.spawn', now() - spawnStartedAt, now(), + !existingSession, ); if (input.fork?.sourceEntryId) { await process.request({ type: 'fork', entryId: input.fork.sourceEntryId }); @@ -287,10 +290,11 @@ export function createPiManagedWorkerOpener( 'rpc.ready', now() - readyStartedAt, now(), + !existingSession, ); const piSessionId = response.data?.sessionId?.trim(); if (!piSessionId) throw new Error('Pi worker did not return a session id'); - if (input.existingSession && input.existingSession.piSessionId !== piSessionId) { + if (existingSession && existingSession.piSessionId !== piSessionId) { throw new CodingRuntimeContractError( 'CODING_SESSION_UNREADABLE', 'Pi reopened a different Conversation session', @@ -327,6 +331,7 @@ export function createPiManagedWorkerOpener( 'session.open', now() - sessionStartedAt, now(), + !existingSession, ); return { worker: new ManagedPiConversationWorker( @@ -356,12 +361,13 @@ function recordManagedMilestone( milestone: Extract, durationMs: number, at: number, + cold: boolean, ): void { listener?.(createPiRuntimeTelemetryEvent({ milestone, conversationId: input.conversation.conversationId, workerGeneration: input.generation, - cold: !input.existingSession, + cold, durationMs, at, })); diff --git a/tests/unit/pi-managed-worker-opener.test.ts b/tests/unit/pi-managed-worker-opener.test.ts index 145ee37..740dd76 100644 --- a/tests/unit/pi-managed-worker-opener.test.ts +++ b/tests/unit/pi-managed-worker-opener.test.ts @@ -123,8 +123,11 @@ describe('managed Pi worker opener', () => { const telemetry: PiRuntimeTelemetryEvent[] = []; const registry = new PiSessionRegistry({ projectStore }); const extensionHost = new PiManagedExtensionHost(); - const opener = createPiManagedWorkerOpener({ - registry, + const createOpener = ( + openerRegistry: PiSessionRegistry, + createSessionKey: () => string = () => 'session-key-a', + ) => createPiManagedWorkerOpener({ + registry: openerRegistry, executablePath: 'electron.exe', cliPath: 'pi-cli.js', userDataDir, @@ -132,7 +135,7 @@ describe('managed Pi worker opener', () => { extensionHost, loadProviderInput: async () => ({ accounts: [account], modelSummaries: [] }), resolveCredential: async () => 'provider-secret-value', - createSessionKey: () => 'session-key-a', + createSessionKey, onTelemetry: (event) => telemetry.push(event), createProcess: (options) => { processOptions.push(options); @@ -140,6 +143,7 @@ describe('managed Pi worker opener', () => { return new OpenerFakeProcess(options.additionalArgs?.[sessionIndex + 1] ?? '', options); }, }); + const opener = createOpener(registry); const first = await opener({ conversation: input, @@ -152,10 +156,23 @@ describe('managed Pi worker opener', () => { revision: { provider: 2, resources: 1 }, existingSession: first.session, }); + const restartedCreateSessionKey = vi.fn(() => 'unexpected-new-session-key'); + const restarted = await createOpener( + new PiSessionRegistry({ projectStore }), + restartedCreateSessionKey, + )({ + conversation: input, + generation: 3, + revision: { provider: 2, resources: 1 }, + }); expect(first.session).toEqual({ piSessionId: 'session-key-a', sessionKey: 'session-key-a' }); expect(reopened.session).toEqual(first.session); - expect(processOptions).toHaveLength(2); + expect(restarted.session).toEqual(first.session); + expect(restartedCreateSessionKey).not.toHaveBeenCalled(); + expect(processOptions).toHaveLength(3); + expect(processOptions[2]?.additionalArgs).toContain('session-key-a'); + expect(processOptions[2]?.additionalArgs).not.toContain('unexpected-new-session-key'); for (const options of processOptions) { const argv = JSON.stringify(options.additionalArgs); expect(argv).toContain('--system-prompt'); @@ -176,6 +193,7 @@ describe('managed Pi worker opener', () => { expect(telemetry.map(({ milestone }) => milestone)).toEqual([ 'resources.ready', 'worker.spawn', 'rpc.ready', 'session.open', 'resources.ready', 'worker.spawn', 'rpc.ready', 'session.open', + 'resources.ready', 'worker.spawn', 'rpc.ready', 'session.open', ]); expect(telemetry.slice(0, 4).every(({ cold }) => cold)).toBe(true); expect(telemetry.slice(4).every(({ cold }) => !cold)).toBe(true); @@ -184,6 +202,7 @@ describe('managed Pi worker opener', () => { expect(JSON.stringify(telemetry)).not.toContain('provider-secret-value'); await first.worker.stop(); await reopened.worker.stop(); + await restarted.worker.stop(); await extensionHost.close(); });