fix(pi): reopen persisted conversation sessions
This commit is contained in:
@@ -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.
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user