fix(pi): reopen persisted conversation sessions

This commit is contained in:
2026-08-25 08:46:55 +08:00
parent e74b59a674
commit 862d1b6d92
3 changed files with 148 additions and 8 deletions

View File

@@ -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.

View File

@@ -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<PiRuntimeMilestone, 'resources.ready' | 'worker.spawn' | 'rpc.ready' | 'session.open'>,
durationMs: number,
at: number,
cold: boolean,
): void {
listener?.(createPiRuntimeTelemetryEvent({
milestone,
conversationId: input.conversation.conversationId,
workerGeneration: input.generation,
cold: !input.existingSession,
cold,
durationMs,
at,
}));

View File

@@ -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();
});