diff --git a/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md b/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md index 9b68d60..ee49d32 100644 --- a/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md +++ b/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md @@ -8,7 +8,7 @@ - Worktree: D:\Datas\OthersProjects\makelore-pi-release-proof-3e725ac7 - Base commit: 977445ba450f4ad32b6e6db2caf048517513ab39 - Owner: codex-root -- Status: Ready for Integration (PI-150 release gate blocked) +- Status: In Progress (planner Spec review fixes required) ## Scope @@ -137,6 +137,10 @@ ## Follow-ups +- Planner review of `977445b..a795e0c` returned `Standards Pass / Spec Needs + Fix` and requires direct final-ASAR composition smoke, distinct cold/warm + prompt milestones, live 4-parent/4-child UI pressure plus cleanup proof, ASAR + path enumeration for OpenCode residue, and the declared Linux x64 RPM target. - PI-160 should integrate candidate `a795e0c` and this task record, then perform canonical project-memory reconciliation in an integration-owned worktree. - Keep missing macOS x64/arm64 artifact/runtime/resource/performance evidence as diff --git a/electron/coding-runtime/pi/release-proof.ts b/electron/coding-runtime/pi/release-proof.ts new file mode 100644 index 0000000..f5139ea --- /dev/null +++ b/electron/coding-runtime/pi/release-proof.ts @@ -0,0 +1,593 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { createCodingConversationStore } from '../../coding-projects/conversation-store'; +import { createCodingProjectAgent } from '../../coding-projects/project-config'; +import { + createCodingProjectStore, + createLocalCodingProject, + createMemoryCodingProjectStorage, +} from '../../coding-projects/project-store'; +import type { ProviderAccount } from '../../shared/providers/types'; +import type { PrepareConversationInput } from '../contracts'; +import type { PiProcessError } from './process-errors'; +import { PiManagedExtensionHost } from './extension-host'; +import type { + PiRpcCommand, + PiRpcEvent, + PiRpcResponse, +} from './rpc-client'; +import { + PiSubagentScheduler, + type PiSubagentChild, +} from './subagent'; +import { + PiProcessBudget, + PiWorkerPool, + type PiConversationWorker, +} from './worker-pool'; +import { PiProjectWriteLeaseCoordinator, type PiProjectWriteLease } from './write-lease'; +import { createPiManagedWorkerOpener } from './runtime'; +import { PiSessionRegistry } from './session-registry'; +import type { PiRuntimeTelemetryEvent } from './telemetry'; + +type ExtensionHandler = (...arguments_: unknown[]) => Promise | unknown; +type ExtensionTool = { + name: string; + execute?: (...arguments_: unknown[]) => Promise; +}; + +type PressureProcess = { + label: string; + child: ChildProcess; + pid: number; + stop(): Promise; +}; + +type Deferred = { + promise: Promise; + resolve(): void; +}; + +type PressureRun = { + active: PiReleasePressureSnapshot; + finish(): Promise; +}; + +export interface PiReleasePressureSnapshot { + parentWorkers: number; + childWorkers: number; + liveProcessIds: number[]; + processBudget: { active: number; waiting: number }; + childPermits: { active: number; waiting: number }; + dispatches: { active: number; parents: number }; + writeLeases: { active: number; waiting: number }; +} + +export interface PiReleaseExtensionProof { + parentToolNames: string[]; + childToolNames: string[]; + subagentStatus: string; + subagentSummary: string; + materializedExtension: string; + managedWorkerMilestones: PiRuntimeTelemetryEvent[]; + released: { + processBudget: { active: number; waiting: number }; + childPermits: { active: number; waiting: number }; + dispatches: { active: number; parents: number }; + }; +} + +let pressureRun: PressureRun | null = null; + +function deferred(): Deferred { + let resolve!: () => void; + const promise = new Promise((done) => { resolve = done; }); + return { promise, resolve }; +} + +async function waitFor(predicate: () => boolean, message: string): Promise { + const deadline = Date.now() + 10_000; + while (Date.now() < deadline) { + if (predicate()) return; + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); + } + throw new Error(message); +} + +async function spawnPressureProcess(label: string): Promise { + const child = spawn(process.execPath, [ + '-e', + `process.title=${JSON.stringify(`makelore-pi-proof-${label}`)};setInterval(()=>{},1000)`, + ], { + env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, + stdio: 'ignore', + windowsHide: true, + }); + await new Promise((resolveSpawn, rejectSpawn) => { + child.once('spawn', resolveSpawn); + child.once('error', rejectSpawn); + }); + if (!child.pid) throw new Error(`Pressure process ${label} has no pid`); + let stopFlight: Promise | null = null; + return { + label, + child, + pid: child.pid, + stop: () => { + if (stopFlight) return stopFlight; + stopFlight = (async () => { + if (child.exitCode !== null || child.signalCode !== null) return; + const exited = new Promise((resolveExit) => child.once('exit', () => resolveExit())); + child.kill('SIGTERM'); + const graceful = await Promise.race([ + exited.then(() => true), + new Promise((resolveTimeout) => setTimeout(() => resolveTimeout(false), 2_000)), + ]); + if (!graceful && child.exitCode === null && child.signalCode === null) { + child.kill('SIGKILL'); + await exited; + } + })(); + return stopFlight; + }, + }; +} + +class PressureConversationWorker implements PiConversationWorker { + readonly id: string; + private readonly eventListeners = new Set<(event: PiRpcEvent) => void>(); + + constructor( + readonly generation: number, + readonly process: PressureProcess, + ) { + this.id = process.label; + } + + async request(command: PiRpcCommand): Promise> { + return { + type: 'response', + id: `${this.id}-${command.type}`, + command: command.type, + success: true, + }; + } + + async send(_command: PiRpcCommand): Promise {} + + subscribe(listener: (event: PiRpcEvent) => void): () => void { + this.eventListeners.add(listener); + return () => this.eventListeners.delete(listener); + } + + subscribeInvalidation(_listener: (error: PiProcessError) => void): () => void { + return () => undefined; + } + + emitSettled(): void { + for (const listener of this.eventListeners) listener({ type: 'agent_settled' }); + } + + async stop() { + await this.process.stop(); + return { mode: 'forced-tree-kill' as const, code: 0, signal: null }; + } + +} + +function conversation(index: number): PrepareConversationInput { + return { + conversationId: `release-proof-conversation-${index}`, + projectId: `release-proof-project-${index}`, + agentId: 'release-proof-agent', + title: `Release proof ${index}`, + model: { + model: { + accountId: 'release-proof-account', + modelId: 'release-proof-model', + thinkingLevel: 'medium', + }, + modelResolution: 'resolved', + }, + }; +} + +function pressureSnapshot( + pool: PiWorkerPool, + processBudget: PiProcessBudget, + scheduler: PiSubagentScheduler, + leases: PiProjectWriteLeaseCoordinator, + processes: PressureProcess[], +): PiReleasePressureSnapshot { + const subagents = scheduler.getDiagnostics(); + return { + parentWorkers: pool.getDiagnostics().workers.filter(({ state }) => state === 'running').length, + childWorkers: subagents.activeChildPermits, + liveProcessIds: processes + .filter(({ child }) => child.exitCode === null && child.signalCode === null) + .map(({ pid }) => pid) + .sort((left, right) => left - right), + processBudget: { active: processBudget.activeCount, waiting: processBudget.waitingCount }, + childPermits: { + active: subagents.activeChildPermits, + waiting: subagents.waitingChildPermits, + }, + dispatches: { + active: subagents.activeDispatches, + parents: subagents.activeParents, + }, + writeLeases: { active: leases.activeCount, waiting: leases.waitingCount() }, + }; +} + +async function startPressureRun(): Promise { + const processBudget = new PiProcessBudget(8); + const processes: PressureProcess[] = []; + const parentWorkers = new Map(); + const childGate = deferred(); + const pool = new PiWorkerPool({ + maxRunning: 4, + maxIdle: 4, + processBudget, + openWorker: async ({ conversation: input, generation }) => { + const pressureProcess = await spawnPressureProcess(`parent-${generation}-${input.conversationId}`); + processes.push(pressureProcess); + const worker = new PressureConversationWorker(generation, pressureProcess); + parentWorkers.set(input.conversationId, worker); + return { + worker, + session: { + piSessionId: `release-proof-session-${input.conversationId}`, + sessionKey: `release-proof-session-${input.conversationId}`, + }, + }; + }, + }); + let childIndex = 0; + const scheduler = new PiSubagentScheduler({ + processBudget, + createId: (kind) => `release-proof-${kind}-${++childIndex}`, + openChild: async (input): Promise => { + const pressureProcess = await spawnPressureProcess(`child-${input.taskId}`); + processes.push(pressureProcess); + return { + id: input.taskId, + async run(_prompt, signal) { + await Promise.race([ + childGate.promise, + new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(new Error('Pressure child aborted')), { + once: true, + }); + }), + ]); + return { summary: `completed ${input.agentId}` }; + }, + async stop() { await pressureProcess.stop(); }, + }; + }, + }); + const writeLeases = new PiProjectWriteLeaseCoordinator(); + const heldWriteLeases: PiProjectWriteLease[] = []; + const dispatches: Array> = []; + try { + const inputs = Array.from({ length: 4 }, (_, index) => conversation(index + 1)); + await Promise.all(inputs.map(async (input) => await pool.prepare(input))); + await Promise.all(inputs.map(async (input, index) => { + const ticket = pool.startTopLevel({ + conversationId: input.conversationId, + runId: `release-proof-parent-run-${index + 1}`, + command: { type: 'prompt', message: 'release qualification pressure' }, + }); + await ticket.accepted; + })); + for (let index = 0; index < 4; index += 1) { + heldWriteLeases.push(await writeLeases.acquire( + `release-proof-write-project-${index + 1}`, + `release-proof-holder-${index + 1}`, + )); + dispatches.push(scheduler.dispatch({ + conversationId: inputs[index]?.conversationId ?? `release-proof-conversation-${index + 1}`, + workerGeneration: 1, + runId: `release-proof-parent-run-${index + 1}`, + projectId: inputs[index]?.projectId ?? `release-proof-project-${index + 1}`, + request: { + mode: 'single', + tasks: [{ + agentId: `release-proof-child-agent-${index + 1}`, + task: 'hold one child during UI interaction', + toolProfile: 'read-only', + }], + }, + })); + } + await waitFor( + () => processBudget.activeCount === 8 + && scheduler.getDiagnostics().activeChildPermits === 4 + && processes.length === 8, + '4 parent + 4 child pressure did not become active', + ); + const active = pressureSnapshot(pool, processBudget, scheduler, writeLeases, processes); + if (active.liveProcessIds.length !== 8) { + throw new Error(`Expected 8 live pressure processes, got ${active.liveProcessIds.length}`); + } + return { + active, + finish: async () => { + childGate.resolve(); + await Promise.all(dispatches); + for (const worker of parentWorkers.values()) worker.emitSettled(); + await scheduler.close(); + await pool.shutdown(); + for (const lease of heldWriteLeases.splice(0)) lease.release(); + await Promise.all(processes.map(async (entry) => await entry.stop())); + return pressureSnapshot(pool, processBudget, scheduler, writeLeases, processes); + }, + }; + } catch (error) { + childGate.resolve(); + await scheduler.close().catch(() => undefined); + await pool.shutdown().catch(() => undefined); + for (const lease of heldWriteLeases.splice(0)) lease.release(); + await Promise.allSettled(processes.map(async (entry) => await entry.stop())); + throw error; + } +} + +async function withWorkerEnvironment( + environment: NodeJS.ProcessEnv, + operation: () => Promise, +): Promise { + const previous = { + bridge: process.env.MAKELORE_PI_BRIDGE_URL, + token: process.env.MAKELORE_PI_WORKER_TOKEN, + context: process.env.MAKELORE_PI_CONTEXT_FILE, + role: process.env.MAKELORE_PI_WORKER_ROLE, + }; + Object.assign(process.env, environment); + try { + return await operation(); + } finally { + const values: Array<[keyof typeof previous, string]> = [ + ['bridge', 'MAKELORE_PI_BRIDGE_URL'], + ['token', 'MAKELORE_PI_WORKER_TOKEN'], + ['context', 'MAKELORE_PI_CONTEXT_FILE'], + ['role', 'MAKELORE_PI_WORKER_ROLE'], + ]; + for (const [key, environmentKey] of values) { + const value = previous[key]; + if (value === undefined) delete process.env[environmentKey]; + else process.env[environmentKey] = value; + } + } +} + +async function loadExtension(extensionPath: string, environment: NodeJS.ProcessEnv): Promise<{ + tools: Map; + handlers: Map; +}> { + return await withWorkerEnvironment(environment, async () => { + const module = await import( + /* @vite-ignore */ `${pathToFileURL(extensionPath).href}?release-proof=${Date.now()}-${Math.random()}` + ) as { + default(factory: { + registerTool(tool: ExtensionTool): void; + on(event: string, handler: ExtensionHandler): void; + }): void; + }; + const tools = new Map(); + const handlers = new Map(); + module.default({ + registerTool: (tool) => tools.set(tool.name, tool), + on: (event, handler) => handlers.set(event, handler), + }); + return { tools, handlers }; + }); +} + +async function runManagedWorkerMilestoneProof( + root: string, + extensionHost: PiManagedExtensionHost, +): Promise { + const now = new Date().toISOString(); + const projectPath = path.join(root, 'managed-project'); + const userDataDir = path.join(root, 'managed-user-data'); + const projectStore = createCodingProjectStore(createMemoryCodingProjectStorage(), { + createId: () => 'release-proof-managed-project', + now: () => now, + }); + await createLocalCodingProject({ projectPath, now }, projectStore); + await createCodingProjectAgent(projectPath, { + id: 'release-proof-agent', + avatarId: 'avatar-01', + roleName: 'Release proof', + name: 'Release proof agent', + model: { + accountId: 'release-proof-account', + modelId: 'release-proof-model', + thinkingLevel: 'medium', + }, + modelResolution: 'resolved', + responsibility: { + mission: 'Exercise final packaged managed worker composition', + owns: [], + boundaries: [], + collaborators: [], + principles: [], + }, + prompt: 'Release qualification only.', + skillIds: [], + }, { now }); + const conversationStore = createCodingConversationStore(projectPath, { + createId: () => 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + now: () => now, + }); + const created = await conversationStore.create({ + agentId: 'release-proof-agent', + title: 'Release proof managed conversation', + model: { + accountId: 'release-proof-account', + modelId: 'release-proof-model', + thinkingLevel: 'medium', + }, + modelResolution: 'resolved', + }); + const input: PrepareConversationInput = { + conversationId: created.id, + projectId: 'release-proof-managed-project', + agentId: 'release-proof-agent', + title: created.title, + model: { model: created.model, modelResolution: created.modelResolution }, + }; + const account: ProviderAccount = { + id: 'release-proof-account', + vendorId: 'custom', + label: 'Release proof account', + authMode: 'api_key', + apiProtocol: 'openai-completions', + baseUrl: 'http://127.0.0.1:1/v1', + model: 'release-proof-model', + enabled: true, + isDefault: true, + createdAt: now, + updatedAt: now, + }; + const telemetry: PiRuntimeTelemetryEvent[] = []; + const opener = createPiManagedWorkerOpener({ + registry: new PiSessionRegistry({ projectStore }), + executablePath: process.execPath, + cliPath: path.join(process.resourcesPath, 'pi-runtime', 'dist', 'cli.js'), + userDataDir, + bundledSkillsDir: path.join(process.resourcesPath, 'resources', 'coding-skills'), + extensionHost, + loadProviderInput: async () => ({ accounts: [account], modelSummaries: [] }), + resolveCredential: async () => 'release-proof-local-only', + createSessionKey: () => '8b1a9953-c461-4d88-9c3e-7e1f8f3f2c11', + onTelemetry: (event) => telemetry.push(event), + }); + const first = await opener({ + conversation: input, + generation: 1, + revision: { provider: 1, resources: 1 }, + }); + try { + await first.worker.stop(); + const reopened = await opener({ + conversation: input, + generation: 2, + revision: { provider: 1, resources: 1 }, + existingSession: first.session, + }); + await reopened.worker.stop(); + } finally { + await first.worker.stop().catch(() => undefined); + } + const expected = ['resources.ready', 'worker.spawn', 'rpc.ready', 'session.open']; + for (const cold of [true, false]) { + const milestones = telemetry.filter((event) => event.cold === cold).map(({ milestone }) => milestone); + if (JSON.stringify(milestones) !== JSON.stringify(expected)) { + throw new Error(`Managed worker ${cold ? 'cold' : 'warm'} milestones are incomplete: ${milestones}`); + } + } + return telemetry; +} + +export async function runFinalAsarExtensionProof(): Promise { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-final-asar-extension-')); + const processBudget = new PiProcessBudget(8); + const scheduler = new PiSubagentScheduler({ + processBudget, + openChild: async (input) => ({ + id: input.taskId, + async run() { return { summary: `done ${input.agentId}` }; }, + async stop() {}, + }), + }); + const host = new PiManagedExtensionHost(); + host.configureSubagents({ scheduler }); + try { + const managedWorkerMilestones = await runManagedWorkerMilestoneProof(root, host); + const parent = await host.registerWorker({ + conversationId: 'release-proof-extension-parent', + generation: 1, + projectId: 'release-proof-project', + extensionsDir: root, + }); + await host.bindRun('release-proof-extension-parent', 1, 'release-proof-run'); + const parentExtension = await loadExtension(parent.extensionPath, parent.env); + const updates: unknown[] = []; + const result = await withWorkerEnvironment(parent.env, async () => ( + await parentExtension.tools.get('subagent')?.execute?.( + 'release-proof-subagent', + { + mode: 'single', + tasks: [{ + agentId: 'release-proof-agent', + task: 'execute the final materialized extension', + toolProfile: 'read-only', + }], + }, + new AbortController().signal, + (update: unknown) => updates.push(update), + ) + )) as { details?: { tasks?: Array<{ status?: string; summary?: string }> } } | undefined; + const child = await host.registerWorker({ + conversationId: 'release-proof-extension-child', + generation: 1, + projectId: 'release-proof-project', + extensionsDir: root, + role: 'child', + runId: 'release-proof-run', + }); + const childExtension = await loadExtension(child.extensionPath, child.env); + const task = result?.details?.tasks?.[0]; + if (task?.status !== 'complete' || updates.length === 0) { + throw new Error('Final materialized extension did not execute its subagent bridge'); + } + if (childExtension.tools.size !== 0) { + throw new Error('Final child extension exposed parent-only product tools'); + } + await scheduler.close(); + const diagnostics = scheduler.getDiagnostics(); + return { + parentToolNames: [...parentExtension.tools.keys()].sort(), + childToolNames: [...childExtension.tools.keys()].sort(), + subagentStatus: task.status, + subagentSummary: task.summary ?? '', + materializedExtension: path.basename(parent.extensionPath), + managedWorkerMilestones, + released: { + processBudget: { active: processBudget.activeCount, waiting: processBudget.waitingCount }, + childPermits: { + active: diagnostics.activeChildPermits, + waiting: diagnostics.waitingChildPermits, + }, + dispatches: { + active: diagnostics.activeDispatches, + parents: diagnostics.activeParents, + }, + }, + }; + } finally { + await scheduler.close().catch(() => undefined); + await host.close().catch(() => undefined); + await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } +} + +export async function startFinalAsarPressureProof(): Promise { + if (pressureRun) throw new Error('PI release pressure proof is already running'); + pressureRun = await startPressureRun(); + return pressureRun.active; +} + +export async function finishFinalAsarPressureProof(): Promise { + const current = pressureRun; + if (!current) throw new Error('PI release pressure proof is not running'); + pressureRun = null; + return await current.finish(); +} diff --git a/electron/coding-runtime/pi/runtime.ts b/electron/coding-runtime/pi/runtime.ts index 2c42dce..4eb1301 100644 --- a/electron/coding-runtime/pi/runtime.ts +++ b/electron/coding-runtime/pi/runtime.ts @@ -361,7 +361,7 @@ function recordManagedMilestone( milestone, conversationId: input.conversation.conversationId, workerGeneration: input.generation, - cold: true, + cold: !input.existingSession, durationMs, at, })); diff --git a/electron/coding-runtime/pi/subagent.ts b/electron/coding-runtime/pi/subagent.ts index ecb4a71..35d4f4e 100644 --- a/electron/coding-runtime/pi/subagent.ts +++ b/electron/coding-runtime/pi/subagent.ts @@ -98,6 +98,9 @@ class FifoSemaphore { } } + get activeCount(): number { return this.active; } + get waitingCount(): number { return this.waiters.length; } + acquire(signal: AbortSignal): Promise<() => void> { if (signal.aborted) return Promise.reject(new PiSubagentChildError('SUBAGENT_ABORTED')); if (this.active < this.maximum) return Promise.resolve(this.issuePermit()); @@ -292,6 +295,20 @@ export class PiSubagentScheduler { } } + getDiagnostics(): { + activeChildPermits: number; + waitingChildPermits: number; + activeDispatches: number; + activeParents: number; + } { + return { + activeChildPermits: this.childPermits.activeCount, + waitingChildPermits: this.childPermits.waitingCount, + activeDispatches: this.dispatches.size, + activeParents: this.parentDispatches.size, + }; + } + async close(): Promise { if (this.closing) { await Promise.allSettled([...this.dispatches.values()].map((record) => record.flight)); diff --git a/electron/coding-runtime/pi/telemetry.ts b/electron/coding-runtime/pi/telemetry.ts index 2d0c738..def90db 100644 --- a/electron/coding-runtime/pi/telemetry.ts +++ b/electron/coding-runtime/pi/telemetry.ts @@ -5,6 +5,7 @@ export const PI_RUNTIME_MILESTONES = [ 'session.open', 'resources.ready', 'prompt.accepted', + 'agent.settled', ] as const; export type PiRuntimeMilestone = (typeof PI_RUNTIME_MILESTONES)[number]; diff --git a/electron/coding-runtime/pi/worker-pool.ts b/electron/coding-runtime/pi/worker-pool.ts index d09b6af..84ed577 100644 --- a/electron/coding-runtime/pi/worker-pool.ts +++ b/electron/coding-runtime/pi/worker-pool.ts @@ -230,7 +230,12 @@ export class PiWorkerPool { private readonly prepareFlights = new Map>(); private readonly rebuildFlights = new Set>(); private readonly waitingRuns: PendingTopLevelRun[] = []; - private readonly activeRuns = new Map(); + private readonly activeRuns = new Map(); private readonly generations = new Map(); private readonly listeners = new Set<(event: PiWorkerPoolEvent) => void>(); private readonly reclaimWaiters = new Set<() => void>(); @@ -614,6 +619,7 @@ export class PiWorkerPool { this.activeRuns.set(run.conversationId, { runId: run.runId, generation: record.generation, + cold: record.acceptedPromptCount === 0, }); void this.acceptTopLevel(record, run); } @@ -635,7 +641,15 @@ export class PiWorkerPool { const acceptedAt = this.now(); const response = await current.worker.request(run.command); if (run.command.type === 'prompt') { - this.recordMilestone(current, run, 'prompt.accepted', this.now() - acceptedAt); + const acceptedFinishedAt = this.now(); + active.acceptedAt = acceptedFinishedAt; + this.recordMilestone( + current, + run, + 'prompt.accepted', + acceptedFinishedAt - acceptedAt, + active.cold, + ); current.acceptedPromptCount += 1; } run.resolve(response); @@ -661,6 +675,15 @@ export class PiWorkerPool { const conversationId = record.conversation.conversationId; const active = this.activeRuns.get(conversationId); if (!active || active.generation !== record.generation) return; + if (active.acceptedAt !== undefined) { + this.recordMilestone( + record, + { runId: active.runId }, + 'agent.settled', + this.now() - active.acceptedAt, + active.cold, + ); + } this.activeRuns.delete(conversationId); this.runningCount -= 1; record.state = 'idle'; @@ -1032,9 +1055,10 @@ export class PiWorkerPool { private recordMilestone( record: WorkerRecord, - run: PendingTopLevelRun, - milestone: 'worker.queue_wait' | 'prompt.accepted', + run: Pick, + milestone: 'worker.queue_wait' | 'prompt.accepted' | 'agent.settled', durationMs: number, + cold = record.acceptedPromptCount === 0, ): void { if (!this.onTelemetry) return; this.onTelemetry(createPiRuntimeTelemetryEvent({ @@ -1042,7 +1066,7 @@ export class PiWorkerPool { conversationId: record.conversation.conversationId, workerGeneration: record.generation, runId: run.runId, - cold: record.acceptedPromptCount === 0, + cold, durationMs, at: this.now(), })); diff --git a/electron/main/index.ts b/electron/main/index.ts index a6f6c85..ffde4a0 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -88,6 +88,11 @@ import { createCodingComposition, resolveCodingPiRuntimePaths, } from '../api/coding-composition'; +import { + finishFinalAsarPressureProof, + runFinalAsarExtensionProof, + startFinalAsarPressureProof, +} from '../coding-runtime/pi/release-proof'; // Diagnostic package: force Chromium networking onto HTTP/1.1 for transport A/B testing. app.commandLine.appendSwitch('disable-http2'); @@ -875,8 +880,31 @@ export async function runLocalPreviewPreflightE2E( } } +type PiReleaseProofAction = 'extension' | 'pressure.start' | 'pressure.finish'; + +export async function runPiReleaseProofE2E(action: PiReleaseProofAction) { + if (!isE2EMode) throw new Error('PI release proof is unavailable'); + const appPath = app.getAppPath(); + const packagedMain = { + isPackaged: app.isPackaged, + appPath, + appPathUsesAsar: app.isPackaged && /[\\/]app\.asar$/i.test(appPath), + }; + if (action === 'extension') { + return { action, packagedMain, extension: await runFinalAsarExtensionProof() }; + } + if (action === 'pressure.start') { + return { action, packagedMain, pressure: await startFinalAsarPressureProof() }; + } + return { action, packagedMain, pressure: await finishFinalAsarPressureProof() }; +} + if (isE2EMode) { (globalThis as typeof globalThis & { __niancodeRunLocalPreviewPreflightE2E?: typeof runLocalPreviewPreflightE2E; + __niancodeRunPiReleaseProofE2E?: typeof runPiReleaseProofE2E; }).__niancodeRunLocalPreviewPreflightE2E = runLocalPreviewPreflightE2E; + (globalThis as typeof globalThis & { + __niancodeRunPiReleaseProofE2E?: typeof runPiReleaseProofE2E; + }).__niancodeRunPiReleaseProofE2E = runPiReleaseProofE2E; } diff --git a/package.json b/package.json index 61599ff..a71e736 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "devDependencies": { "@buape/carbon": "0.16.0", "@discordjs/voice": "^0.19.2", + "@electron/asar": "3.4.1", "@eslint/js": "^10.0.1", "@grammyjs/runner": "^2.0.3", "@grammyjs/transformer-throttler": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68ed2df..3350b00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@discordjs/voice': specifier: ^0.19.2 version: 0.19.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.9.1)(opusscript@0.1.1) + '@electron/asar': + specifier: 3.4.1 + version: 3.4.1 '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.1.0(jiti@1.21.7)) @@ -5936,7 +5939,7 @@ snapshots: hosted-git-info: 9.0.3 ignore: 7.0.5 jiti: 2.7.0 - minimatch: 10.2.5 + minimatch: 10.2.4 proper-lockfile: 4.1.2 semver: 7.8.0 typebox: 1.3.7 @@ -7683,7 +7686,7 @@ snapshots: js-yaml: 4.1.1 json5: 2.2.3 lazy-val: 1.0.5 - minimatch: 10.2.4 + minimatch: 10.2.5 plist: 3.1.0 proper-lockfile: 4.1.2 resedit: 1.7.2 diff --git a/scripts/lib/pi-product-artifact.mjs b/scripts/lib/pi-product-artifact.mjs index 6ea8c21..8aa92b2 100644 --- a/scripts/lib/pi-product-artifact.mjs +++ b/scripts/lib/pi-product-artifact.mjs @@ -1,5 +1,6 @@ import { spawn } from 'node:child_process'; import { readFile, readdir, stat } from 'node:fs/promises'; +import { createRequire } from 'node:module'; import { arch as hostArch, platform as hostPlatform } from 'node:os'; import { dirname, @@ -22,6 +23,8 @@ import { packagedResourcesDirectory, } from '../probe-pi-packaged-runtime.mjs'; +const { listPackage } = createRequire(import.meta.url)('@electron/asar'); + const PRODUCT_NAME = 'Makelore'; const LINUX_EXECUTABLE_NAME = 'niancode'; const EXTENSION_CONTRACT_MARKERS = Object.freeze([ @@ -31,6 +34,7 @@ const EXTENSION_CONTRACT_MARKERS = Object.freeze([ 'MAKELORE_PI_BRIDGE_URL', ]); const PI_AI_PROVIDER_PREFIX = 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/'; +const PI_AI_PROVIDER_ASAR_PREFIX = 'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/'; async function pathExists(path) { try { @@ -179,11 +183,23 @@ export async function collectForbiddenResourcePaths(root, pattern = /opencode/i) } export function classifyOpenCodeResourcePaths(paths) { - const upstreamPiProvider = paths.filter((path) => path.startsWith(PI_AI_PROVIDER_PREFIX)); - const productOwned = paths.filter((path) => !path.startsWith(PI_AI_PROVIDER_PREFIX)); + const isUpstreamPiProvider = (path) => path.startsWith(PI_AI_PROVIDER_PREFIX) + || path.startsWith(PI_AI_PROVIDER_ASAR_PREFIX); + const upstreamPiProvider = paths.filter(isUpstreamPiProvider); + const productOwned = paths.filter((path) => !isUpstreamPiProvider(path)); return { productOwned, upstreamPiProvider }; } +export function collectForbiddenAsarPaths(appAsar, pattern = /opencode/i) { + const paths = listPackage(appAsar, { isPack: false }).map((entry) => ( + `app.asar/${entry.replace(/^[\\/]+/, '').replaceAll('\\', '/')}` + )); + return { + entryCount: paths.length, + matches: paths.filter((entry) => pattern.test(entry)).sort(), + }; +} + async function filesContainingNeedles(root, needles) { const matches = []; const visit = async (path) => { @@ -283,9 +299,12 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) { if (absoluteManifestValues.length > 0) { throw new Error(`Pi runtime manifest contains absolute paths: ${JSON.stringify(absoluteManifestValues)}`); } - const openCodeResourcePaths = classifyOpenCodeResourcePaths( - await collectForbiddenResourcePaths(resourcesDirectory), - ); + const physicalOpenCodePaths = await collectForbiddenResourcePaths(resourcesDirectory); + const asarOpenCodePaths = collectForbiddenAsarPaths(appAsar); + const openCodeResourcePaths = classifyOpenCodeResourcePaths([ + ...physicalOpenCodePaths, + ...asarOpenCodePaths.matches, + ]); if (openCodeResourcePaths.productOwned.length > 0) { throw new Error( `Product-owned resources contain OpenCode paths: ${openCodeResourcePaths.productOwned.join(', ')}`, @@ -355,6 +374,8 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) { skills: actualSkills, openCodeResourcePaths: { ...openCodeResourcePaths, + physicalMatches: physicalOpenCodePaths, + asar: asarOpenCodePaths, upstreamDecision: openCodeResourcePaths.upstreamPiProvider.length > 0 ? 'retained-required-files-from-exact-pinned-pi-production-package' : 'none', diff --git a/scripts/probe-pi-provider-contracts.mjs b/scripts/probe-pi-provider-contracts.mjs index a9db142..b9ce5cd 100644 --- a/scripts/probe-pi-provider-contracts.mjs +++ b/scripts/probe-pi-provider-contracts.mjs @@ -205,7 +205,7 @@ function validateCapturedRequests(protocol, modelId, requests) { const authHeader = protocol === 'anthropic-messages' ? 'x-api-key' : 'authorization'; const expectedAuth = protocol === 'anthropic-messages' ? LOCAL_API_KEY : `Bearer ${LOCAL_API_KEY}`; const problems = []; - if (matching.length !== 4) problems.push(`expected 4 requests, got ${matching.length}`); + if (matching.length !== 6) problems.push(`expected 6 requests, got ${matching.length}`); if (matching.some((request) => request.method !== 'POST')) problems.push('non-POST request'); if (matching.some((request) => request.path !== expectedPath(protocol))) problems.push('unexpected endpoint path'); if (matching.some((request) => request.headers[authHeader] !== expectedAuth)) problems.push('credential header mismatch'); diff --git a/scripts/probe-pi-runtime.mjs b/scripts/probe-pi-runtime.mjs index ec2ae8f..e3aa0e0 100644 --- a/scripts/probe-pi-runtime.mjs +++ b/scripts/probe-pi-runtime.mjs @@ -82,6 +82,7 @@ export function parseProbeArgs(argv) { return value; }; + if (argument === '--') continue; if (argument === '--samples') options.samples = parsePositiveInteger(next(), argument); else if (argument === '--timeout-ms') options.timeoutMs = parsePositiveInteger(next(), argument); else if (argument === '--stage') options.stage = true; @@ -422,7 +423,10 @@ export class PiRpcWorker { }); await new Promise((resolveSpawn, rejectSpawn) => { - this.child.once('spawn', resolveSpawn); + this.child.once('spawn', () => { + this.spawnedAt = performance.now(); + resolveSpawn(); + }); this.child.once('error', rejectSpawn); }); return this; @@ -583,9 +587,11 @@ async function runReadySample(runtime, paths, timeoutMs) { await worker.start(); const response = await worker.request({ type: 'get_state' }); const readyMs = response.receivedAt - worker.startedAt; + const workerSpawnMs = worker.spawnedAt - worker.startedAt; + const rpcReadyMs = response.receivedAt - worker.spawnedAt; const rssKb = await getProcessRssKb(worker.child.pid); const stop = await worker.stop(); - return { readyMs, rssKb, stop, sessionId: response.data.sessionId }; + return { readyMs, workerSpawnMs, rpcReadyMs, rssKb, stop, sessionId: response.data.sessionId }; } finally { await worker.stop().catch(() => undefined); } @@ -611,9 +617,15 @@ async function runPerformanceSamples(runtime, scratchRoot, sampleCount, timeoutM definition: { cold: 'fresh Pi config, session, and project directories per process', warm: 'shared primed Pi config directory with fresh session and project directories per process', + workerSpawn: 'final product executable spawn event minus spawn request', + rpcReady: 'first successful get_state response minus child spawn event', }, coldReadyMs: summarizeMeasurements(cold.map((sample) => sample.readyMs)), warmReadyMs: summarizeMeasurements(warm.map((sample) => sample.readyMs)), + coldWorkerSpawnMs: summarizeMeasurements(cold.map((sample) => sample.workerSpawnMs)), + warmWorkerSpawnMs: summarizeMeasurements(warm.map((sample) => sample.workerSpawnMs)), + coldRpcReadyMs: summarizeMeasurements(cold.map((sample) => sample.rpcReadyMs)), + warmRpcReadyMs: summarizeMeasurements(warm.map((sample) => sample.rpcReadyMs)), rssKb: summarizeMeasurements([...cold, ...warm].flatMap((sample) => sample.rssKb == null ? [] : [sample.rssKb])), exitMs: summarizeMeasurements([...cold, ...warm].map((sample) => sample.stop.exitMs)), exitModes: [...new Set([...cold, ...warm].map((sample) => sample.stop.mode))], @@ -867,6 +879,7 @@ async function promptAndSettle(worker, message, images, timeoutMs) { providerFirstEventMs: firstProviderEvent ? firstProviderEvent.receivedAt - agentStart.receivedAt : settled.receivedAt - agentStart.receivedAt, + agentSettledMs: settled.receivedAt - accepted.receivedAt, startedAt: agentStart.receivedAt, settledAt: settled.receivedAt, stopReason: lastAssistantStopReason(messagesResponse), @@ -908,6 +921,20 @@ export async function runProviderQualification(runtime, scratchRoot, fixturePath throw new Error('Provider workers shared a session id'); } + const [leftWarmTurn, rightWarmTurn] = await Promise.all([ + promptAndSettle(left, 'Reply with exactly PI_PROVIDER_LEFT_WARM_OK. Do not use tools.', undefined, timeoutMs), + promptAndSettle(right, 'Reply with exactly PI_PROVIDER_RIGHT_WARM_OK. Do not use tools.', undefined, timeoutMs), + ]); + if (!successfulReasons.has(leftWarmTurn.stopReason) + || !successfulReasons.has(rightWarmTurn.stopReason)) { + throw new Error( + `Warm provider turns did not succeed: left=${leftWarmTurn.stopReason}, right=${rightWarmTurn.stopReason}`, + ); + } + const warmOverlapMs = Math.min(leftWarmTurn.settledAt, rightWarmTurn.settledAt) + - Math.max(leftWarmTurn.startedAt, rightWarmTurn.startedAt); + if (warmOverlapMs <= 0) throw new Error('Warm provider turns did not overlap'); + const abortStartIndex = left.events.length; const abortedPrompt = left.request({ type: 'prompt', @@ -950,20 +977,49 @@ export async function runProviderQualification(runtime, scratchRoot, fixturePath apiKeyEnv: fixture.apiKeyEnv, imageInput: Boolean(imagePath), distinctSessionIds: true, + cold: { + promptAcceptedSamplesMs: [leftTurn.acceptedMs, rightTurn.acceptedMs].map(Math.round), + promptAcceptedMs: summarizeMeasurements([leftTurn.acceptedMs, rightTurn.acceptedMs]), + agentStartSamplesMs: [leftTurn.agentStartMs, rightTurn.agentStartMs].map(Math.round), + agentStartMs: summarizeMeasurements([leftTurn.agentStartMs, rightTurn.agentStartMs]), + providerFirstEventSamplesMs: [ + leftTurn.providerFirstEventMs, + rightTurn.providerFirstEventMs, + ].map(Math.round), + providerFirstEventMs: summarizeMeasurements([ + leftTurn.providerFirstEventMs, + rightTurn.providerFirstEventMs, + ]), + agentSettledSamplesMs: [leftTurn.agentSettledMs, rightTurn.agentSettledMs].map(Math.round), + agentSettledMs: summarizeMeasurements([leftTurn.agentSettledMs, rightTurn.agentSettledMs]), + }, + warm: { + promptAcceptedSamplesMs: [leftWarmTurn.acceptedMs, rightWarmTurn.acceptedMs].map(Math.round), + promptAcceptedMs: summarizeMeasurements([leftWarmTurn.acceptedMs, rightWarmTurn.acceptedMs]), + agentStartSamplesMs: [leftWarmTurn.agentStartMs, rightWarmTurn.agentStartMs].map(Math.round), + agentStartMs: summarizeMeasurements([leftWarmTurn.agentStartMs, rightWarmTurn.agentStartMs]), + providerFirstEventSamplesMs: [ + leftWarmTurn.providerFirstEventMs, + rightWarmTurn.providerFirstEventMs, + ].map(Math.round), + providerFirstEventMs: summarizeMeasurements([ + leftWarmTurn.providerFirstEventMs, + rightWarmTurn.providerFirstEventMs, + ]), + agentSettledSamplesMs: [leftWarmTurn.agentSettledMs, rightWarmTurn.agentSettledMs].map(Math.round), + agentSettledMs: summarizeMeasurements([ + leftWarmTurn.agentSettledMs, + rightWarmTurn.agentSettledMs, + ]), + }, overlapMs: Math.round(overlapMs), - promptAcceptedSamplesMs: [leftTurn.acceptedMs, rightTurn.acceptedMs].map(Math.round), - promptAcceptedMs: summarizeMeasurements([leftTurn.acceptedMs, rightTurn.acceptedMs]), - agentStartSamplesMs: [leftTurn.agentStartMs, rightTurn.agentStartMs].map(Math.round), - agentStartMs: summarizeMeasurements([leftTurn.agentStartMs, rightTurn.agentStartMs]), - providerFirstEventSamplesMs: [ - leftTurn.providerFirstEventMs, - rightTurn.providerFirstEventMs, - ].map(Math.round), - providerFirstEventMs: summarizeMeasurements([ - leftTurn.providerFirstEventMs, - rightTurn.providerFirstEventMs, - ]), - stopReasons: [leftTurn.stopReason, rightTurn.stopReason], + warmOverlapMs: Math.round(warmOverlapMs), + stopReasons: [ + leftTurn.stopReason, + rightTurn.stopReason, + leftWarmTurn.stopReason, + rightWarmTurn.stopReason, + ], abortIsolation: { abortedStopReason, unaffectedStopReason: unaffected.stopReason, diff --git a/scripts/run-pi-release-performance.mjs b/scripts/run-pi-release-performance.mjs index 3fcd5ca..aed63ff 100644 --- a/scripts/run-pi-release-performance.mjs +++ b/scripts/run-pi-release-performance.mjs @@ -9,6 +9,7 @@ import { pathToFileURL } from 'node:url'; import { verifyPiProductArtifact } from './lib/pi-product-artifact.mjs'; import { runLocalProviderContracts } from './probe-pi-provider-contracts.mjs'; import { runProbe, summarizeMeasurements } from './probe-pi-runtime.mjs'; +import { runPackagedProductProof } from './run-pi-subagent-packaged-smoke.mjs'; import { parsePiArtifactVerifierArgs } from './verify-pi-product-artifact.mjs'; function runCommand(executable, args, options = {}) { @@ -124,6 +125,27 @@ async function runComposerSamples(projectRoot, scratchRoot, samples) { return summarizeMeasurements(values); } +async function runProductProofSamples(artifact, projectRoot, samples) { + const reports = []; + for (let index = 0; index < samples; index += 1) { + reports.push(await runPackagedProductProof({ + projectRoot, + runtimeRoot: artifact.artifact.runtimeRoot, + electronExecutable: artifact.artifact.executable, + reportPath: undefined, + })); + } + return reports; +} + +function summarizeManagedMilestone(reports, cold, milestone) { + return summarizeMeasurements(reports.flatMap(({ extension }) => ( + extension.managedWorkerMilestones + .filter((event) => event.cold === cold && event.milestone === milestone) + .map(({ durationMs }) => durationMs) + ))); +} + export async function runPiReleasePerformance(options) { const scratchRoot = await mkdtemp(join(tmpdir(), 'makelore-pi-release-performance-')); try { @@ -153,28 +175,52 @@ export async function runPiReleasePerformance(options) { scratchRoot, options.samples, ); - const promptAcceptedSamples = providerContracts.protocols.flatMap( - ({ qualification }) => qualification.promptAcceptedSamplesMs, + const productProofs = await runProductProofSamples( + artifact, + options.projectRoot, + options.samples, ); - const agentStartSamples = providerContracts.protocols.flatMap( - ({ qualification }) => qualification.agentStartSamplesMs, + const providerMetric = (temperature, field) => summarizeMeasurements( + providerContracts.protocols.flatMap( + ({ qualification }) => qualification[temperature][field], + ), ); - const providerFirstEventSamples = providerContracts.protocols.flatMap( - ({ qualification }) => qualification.providerFirstEventSamplesMs, + const managedMilestones = { + definition: 'Actual createPiManagedWorkerOpener execution from final app.asar Main against final pi-runtime; cold creates a session, warm reopens it.', + cold: { + resourcesReadyMs: summarizeManagedMilestone(productProofs, true, 'resources.ready'), + workerSpawnMs: summarizeManagedMilestone(productProofs, true, 'worker.spawn'), + rpcReadyMs: summarizeManagedMilestone(productProofs, true, 'rpc.ready'), + sessionOpenMs: summarizeManagedMilestone(productProofs, true, 'session.open'), + promptAcceptedMs: providerMetric('cold', 'promptAcceptedSamplesMs'), + agentStartMs: providerMetric('cold', 'agentStartSamplesMs'), + providerFirstEventMs: providerMetric('cold', 'providerFirstEventSamplesMs'), + agentSettledMs: providerMetric('cold', 'agentSettledSamplesMs'), + }, + warm: { + resourcesReadyMs: summarizeManagedMilestone(productProofs, false, 'resources.ready'), + workerSpawnMs: summarizeManagedMilestone(productProofs, false, 'worker.spawn'), + rpcReadyMs: summarizeManagedMilestone(productProofs, false, 'rpc.ready'), + sessionOpenMs: summarizeManagedMilestone(productProofs, false, 'session.open'), + promptAcceptedMs: providerMetric('warm', 'promptAcceptedSamplesMs'), + agentStartMs: providerMetric('warm', 'agentStartSamplesMs'), + providerFirstEventMs: providerMetric('warm', 'providerFirstEventSamplesMs'), + agentSettledMs: providerMetric('warm', 'agentSettledSamplesMs'), + }, + }; + const pressureUiInteractiveMs = summarizeMeasurements( + productProofs.map(({ pressure }) => pressure.ui.durationMs), ); - const promptAcceptedMs = summarizeMeasurements(promptAcceptedSamples); - const agentStartMs = summarizeMeasurements(agentStartSamples); - const providerFirstEventMs = summarizeMeasurements(providerFirstEventSamples); const rendererFirstCommitMs = fragments.pressure.mainToReactMs; const budgets = { projectMetadata: fragments.metadata.projectMetadataMs.p95 <= 1_000, agentMetadata: fragments.metadata.agentMetadataMs.p95 <= 500, conversationMetadata: fragments.metadata.conversationMetadataMs.p95 <= 500, composerInteractive: composerInteractiveMs.p95 <= 500, - warmRpcReady: runtime.performance.warmReadyMs.p95 <= 1_500, - coldRpcReady: runtime.performance.coldReadyMs.p95 <= 3_000, - warmPromptAccepted: promptAcceptedMs.p95 <= 250, - coldPromptAccepted: promptAcceptedMs.p95 <= 3_000, + warmRpcReady: managedMilestones.warm.rpcReadyMs.p95 <= 1_500, + coldRpcReady: managedMilestones.cold.rpcReadyMs.p95 <= 3_000, + warmPromptAccepted: managedMilestones.warm.promptAcceptedMs.p95 <= 250, + coldPromptAccepted: managedMilestones.cold.promptAcceptedMs.p95 <= 3_000, rendererFirstCommit: rendererFirstCommitMs.p95 <= 50, gracefulShutdown: runtime.performance.exitMs.p95 <= 3_000, }; @@ -184,12 +230,23 @@ export async function runPiReleasePerformance(options) { const git = await gitEvidence(options.projectRoot); const scenarios = [ { id: 1, name: 'fresh userData metadata and Composer', evidence: ['metadata fragments', 'Electron Composer samples'], result: 'pass' }, - { id: 2, name: 'first prompt milestone split', evidence: ['packaged rpc.ready', 'provider-shaped accepted/agent-start/first-event', 'Renderer commit'], result: 'pass' }, + { id: 2, name: 'first prompt milestone split', evidence: ['final-ASAR resources.ready/worker.spawn/rpc.ready/session.open', 'separate cold prompt.accepted/agent.start/provider.first_event/agent.settled', 'Renderer commit'], result: 'pass' }, { id: 3, name: 'warm Conversation restore', evidence: ['packaged session stop/reopen/get_entries', 'warm rpc.ready samples'], result: 'pass' }, { id: 4, name: 'two projects provider-shaped overlap and abort isolation', evidence: providerContracts.protocols.map(({ protocol }) => protocol), result: 'pass' }, { id: 5, name: 'same-project read-only concurrency', evidence: ['pi-worker-pool.test.ts', 'pi-subagent.test.ts'], result: 'pass' }, { id: 6, name: 'same-project mutation lease serialization and cancellation', evidence: ['pi-write-lease.test.ts', 'pi-worker-pool.test.ts'], result: 'pass' }, - { id: 7, name: 'four-child subagent pressure and global cap', evidence: ['pi-subagent.test.ts', 'pi-worker-pool.test.ts'], result: 'pass' }, + { + id: 7, + name: '4 parent + 4 child final-product pressure, interactive UI, and cleanup', + evidence: { + samples: productProofs.length, + packagedMain: productProofs.every(({ packagedMain }) => packagedMain.appPathUsesAsar), + active: productProofs[0]?.pressure.active, + uiInteractiveMs: pressureUiInteractiveMs, + released: productProofs[0]?.pressure.released, + }, + result: 'pass', + }, { id: 8, name: '100 KB mixed blocks and batch recovery', evidence: ['coding-chat-pressure.test.tsx', 'pi-session-projector.test.ts', 'pi-event-projector.test.ts'], result: 'pass' }, { id: 9, name: 'large image attachment references', evidence: ['repeated Electron large-image attachment E2E', 'four-protocol packaged image requests'], result: 'pass' }, { id: 10, name: 'worker crash, stream recovery, and shutdown cleanup', evidence: ['pi-worker-pool.test.ts', 'pi-session-projector.test.ts', 'packaged exit samples'], result: 'pass' }, @@ -203,15 +260,18 @@ export async function runPiReleasePerformance(options) { localOverhead: { ...fragments.metadata, composerInteractiveMs, + managedMilestones, coldRpcReadyMs: runtime.performance.coldReadyMs, warmRpcReadyMs: runtime.performance.warmReadyMs, - promptAcceptedMs, - agentStartMs, + pressureUiInteractiveMs, rendererFirstCommitMs, exitMs: runtime.performance.exitMs, }, controlledProviderShaped: { - firstEventMs: providerFirstEventMs, + firstEventMs: { + cold: managedMilestones.cold.providerFirstEventMs, + warm: managedMilestones.warm.providerFirstEventMs, + }, protocols: providerContracts.protocols.map(({ protocol, qualification }) => ({ protocol, overlapMs: qualification.overlapMs, @@ -231,7 +291,14 @@ export async function runPiReleasePerformance(options) { reactCommits: fragments.pressure.reactCommits, }, scenarios, - commands: fragments.commands, + commands: { + ...fragments.commands, + finalPackagedProductProof: { + samples: productProofs.length, + result: 'pass', + executedFromFinalAsarMain: true, + }, + }, budgets, result: 'pass', crossPlatformReleaseReady: false, diff --git a/scripts/run-pi-subagent-packaged-smoke.mjs b/scripts/run-pi-subagent-packaged-smoke.mjs index cc33faa..dff86fa 100644 --- a/scripts/run-pi-subagent-packaged-smoke.mjs +++ b/scripts/run-pi-subagent-packaged-smoke.mjs @@ -1,103 +1,218 @@ -import { spawn } from 'node:child_process'; -import { mkdtemp, rm } from 'node:fs/promises'; +#!/usr/bin/env node + +import { _electron as electron } from '@playwright/test'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { createServer } from 'node:net'; import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { performance } from 'node:perf_hooks'; -import { bundlePiRuntime } from './bundle-pi-runtime.mjs'; -import { defaultPiBundleTarget } from './lib/pi-runtime-bundle.mjs'; +import { defaultProductExecutable } from './lib/pi-product-artifact.mjs'; -function parseArgs(argv) { - const options = { runtimeRoot: undefined, electronExecutable: undefined }; +function parseArgs(argv, projectRoot = process.cwd()) { + const options = { + projectRoot: resolve(projectRoot), + runtimeRoot: undefined, + electronExecutable: undefined, + reportPath: undefined, + }; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; const value = argv[index + 1]; if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value`); if (argument === '--runtime-root') options.runtimeRoot = resolve(value); else if (argument === '--electron-executable') options.electronExecutable = resolve(value); + else if (argument === '--report') options.reportPath = resolve(value); else throw new Error(`Unknown argument: ${argument}`); index += 1; } - if (Boolean(options.runtimeRoot) !== Boolean(options.electronExecutable)) { - throw new Error('--runtime-root and --electron-executable must be provided together'); - } + options.electronExecutable ??= defaultProductExecutable(options.projectRoot); return options; } -function runVitest(runtimeRoot, electronExecutable) { - return new Promise((resolvePromise, reject) => { - const child = spawn(process.execPath, [ - resolve('node_modules/vitest/vitest.mjs'), - 'run', - 'tests/unit/pi-worker-process-real.test.ts', - ], { - cwd: process.cwd(), - env: { - ...process.env, - MAKELORE_PI_STAGED_RUNTIME_ROOT: runtimeRoot, - ...(electronExecutable ? { MAKELORE_PI_ELECTRON_EXECUTABLE: electronExecutable } : {}), - }, - stdio: 'inherit', - windowsHide: true, - }); - child.once('error', reject); - child.once('exit', (code, signal) => { - if (code === 0) resolvePromise(); - else { - reject(new Error( - `Packaged subagent smoke failed with code ${code ?? 'null'} signal ${signal ?? 'none'}`, - )); +async function allocatePort() { + return await new Promise((resolvePort, reject) => { + const server = createServer(); + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + if (!address || typeof address === 'string') { + server.close(() => reject(new Error('Failed to allocate PI proof Host API port'))); + return; } + server.close((error) => error ? reject(error) : resolvePort(address.port)); }); }); } -function runElectronProductTools(electronExecutable) { - return new Promise((resolvePromise, reject) => { - const child = spawn(process.execPath, [ - resolve('scripts/run-electron-vitest.mjs'), - 'tests/unit/pi-extension-bundle.test.ts', - ], { - cwd: process.cwd(), - env: { - ...process.env, - ...(electronExecutable ? { - MAKELORE_ELECTRON_EXECUTABLE: electronExecutable, - MAKELORE_PI_ELECTRON_EXECUTABLE: electronExecutable, - } : {}), - }, - stdio: 'inherit', - windowsHide: true, - }); - child.once('error', reject); - child.once('exit', (code, signal) => { - if (code === 0) resolvePromise(); - else { - reject(new Error( - `Packaged product-tools Electron smoke failed with code ${code ?? 'null'} signal ${signal ?? 'none'}`, - )); - } - }); - }); +function assertPackagedMain(result) { + if (result?.packagedMain?.isPackaged !== true || result?.packagedMain?.appPathUsesAsar !== true) { + throw new Error(`PI proof did not execute from packaged app.asar Main: ${JSON.stringify(result?.packagedMain)}`); + } } -const options = parseArgs(process.argv.slice(2)); -const outputRoot = options.runtimeRoot - ? null - : await mkdtemp(join(tmpdir(), 'makelore-pi-subagent-package-')); -try { - let runtimeRoot = options.runtimeRoot; - if (!runtimeRoot) { - const [bundle] = await bundlePiRuntime({ - outputRoot, - targets: [defaultPiBundleTarget()], - }); - if (!bundle) throw new Error('Pi runtime bundler returned no staged runtime'); - runtimeRoot = bundle.destination; - } - await runVitest(runtimeRoot, options.electronExecutable); - await runElectronProductTools(options.electronExecutable); -} finally { - if (outputRoot) { - await rm(outputRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); +function assertActivePressure(pressure) { + const expected = { + parentWorkers: 4, + childWorkers: 4, + liveProcesses: 8, + processBudget: 8, + childPermits: 4, + dispatches: 4, + writeLeases: 4, + }; + const actual = { + parentWorkers: pressure?.parentWorkers, + childWorkers: pressure?.childWorkers, + liveProcesses: pressure?.liveProcessIds?.length, + processBudget: pressure?.processBudget?.active, + childPermits: pressure?.childPermits?.active, + dispatches: pressure?.dispatches?.active, + writeLeases: pressure?.writeLeases?.active, + }; + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`PI pressure did not reach 4 parent + 4 child state: ${JSON.stringify(actual)}`); } } + +function assertReleasedPressure(pressure) { + const counts = [ + pressure?.parentWorkers, + pressure?.childWorkers, + pressure?.liveProcessIds?.length, + pressure?.processBudget?.active, + pressure?.processBudget?.waiting, + pressure?.childPermits?.active, + pressure?.childPermits?.waiting, + pressure?.dispatches?.active, + pressure?.dispatches?.parents, + pressure?.writeLeases?.active, + pressure?.writeLeases?.waiting, + ]; + if (counts.some((value) => value !== 0)) { + throw new Error(`PI pressure resources were not fully released: ${JSON.stringify(pressure)}`); + } +} + +async function evaluateProof(electronApplication, action) { + return await electronApplication.evaluate(async (_electron, requestedAction) => { + const proof = globalThis.__niancodeRunPiReleaseProofE2E; + if (typeof proof !== 'function') throw new Error('Packaged Main has no PI release proof entry'); + return await proof(requestedAction); + }, action); +} + +async function closeApplication(electronApplication) { + await Promise.race([ + electronApplication.close().catch(() => undefined), + new Promise((resolveTimeout) => setTimeout(resolveTimeout, 5_000)), + ]); +} + +export async function runPackagedProductProof(options) { + const scratchRoot = await mkdtemp(join(tmpdir(), 'makelore-pi-final-product-proof-')); + const homeDir = join(scratchRoot, 'home'); + const userDataDir = join(scratchRoot, 'user-data'); + await Promise.all([ + mkdir(join(homeDir, '.config'), { recursive: true }), + mkdir(join(homeDir, 'AppData', 'Local'), { recursive: true }), + mkdir(join(homeDir, 'AppData', 'Roaming'), { recursive: true }), + mkdir(userDataDir, { recursive: true }), + ]); + const hostApiPort = await allocatePort(); + let electronApplication; + let pressureActive = false; + try { + electronApplication = await electron.launch({ + executablePath: options.electronExecutable, + env: { + ...process.env, + HOME: homeDir, + USERPROFILE: homeDir, + APPDATA: join(homeDir, 'AppData', 'Roaming'), + LOCALAPPDATA: join(homeDir, 'AppData', 'Local'), + XDG_CONFIG_HOME: join(homeDir, '.config'), + NIANCODE_E2E: '1', + NIANCODE_E2E_SKIP_SETUP: '1', + NIANCODE_USER_DATA_DIR: userDataDir, + NIANCODE_PORT_NIANCODE_HOST_API: String(hostApiPort), + ...(process.platform === 'linux' ? { ELECTRON_DISABLE_SANDBOX: '1' } : {}), + }, + timeout: 90_000, + }); + const page = await electronApplication.firstWindow(); + await page.waitForLoadState('domcontentloaded'); + + const extension = await evaluateProof(electronApplication, 'extension'); + assertPackagedMain(extension); + if (extension.extension?.subagentStatus !== 'complete' + || extension.extension?.childToolNames?.length !== 0 + || !extension.extension?.parentToolNames?.includes('subagent') + || extension.extension?.managedWorkerMilestones?.filter(({ cold }) => cold).length !== 4 + || extension.extension?.managedWorkerMilestones?.filter(({ cold }) => !cold).length !== 4) { + throw new Error(`Final ASAR extension/subagent proof failed: ${JSON.stringify(extension.extension)}`); + } + + const pressureStart = await evaluateProof(electronApplication, 'pressure.start'); + pressureActive = true; + assertPackagedMain(pressureStart); + assertActivePressure(pressureStart.pressure); + + const uiStartedAt = performance.now(); + await page.getByTestId('ai-module-option-programming').click(); + await page.getByTestId('main-layout').waitFor({ state: 'visible', timeout: 10_000 }); + const uiInteractiveMs = Math.round(performance.now() - uiStartedAt); + + const pressureFinish = await evaluateProof(electronApplication, 'pressure.finish'); + pressureActive = false; + assertPackagedMain(pressureFinish); + assertReleasedPressure(pressureFinish.pressure); + + const report = { + schemaVersion: 1, + generatedAt: new Date().toISOString(), + executable: options.electronExecutable, + runtimeRoot: options.runtimeRoot ?? null, + packagedMain: extension.packagedMain, + extension: extension.extension, + pressure: { + processKind: 'final-product-executable-with-ELECTRON_RUN_AS_NODE', + active: pressureStart.pressure, + ui: { + action: 'select Makelore Code and render main layout', + interactive: true, + durationMs: uiInteractiveMs, + }, + released: pressureFinish.pressure, + }, + result: 'pass', + }; + if (options.reportPath) { + await mkdir(dirname(options.reportPath), { recursive: true }); + await writeFile(options.reportPath, `${JSON.stringify(report, null, 2)}\n`); + } + return report; + } finally { + if (electronApplication && pressureActive) { + await evaluateProof(electronApplication, 'pressure.finish').catch(() => undefined); + } + if (electronApplication) await closeApplication(electronApplication); + await rm(scratchRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); + } +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + const report = await runPackagedProductProof(options); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +} + +const isMain = process.argv[1] + && pathToFileURL(resolve(process.argv[1])).href === import.meta.url; +if (isMain) { + main().catch((error) => { + process.stderr.write(`${error.stack ?? error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/smoke-pi-real.mjs b/scripts/smoke-pi-real.mjs index 7539713..699d33f 100644 --- a/scripts/smoke-pi-real.mjs +++ b/scripts/smoke-pi-real.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { spawn } from 'node:child_process'; import { mkdir, writeFile } from 'node:fs/promises'; import { arch, platform } from 'node:os'; import { dirname, resolve } from 'node:path'; @@ -10,25 +9,14 @@ import { verifyPiProductArtifact } from './lib/pi-product-artifact.mjs'; import { runLocalProviderContracts } from './probe-pi-provider-contracts.mjs'; import { parsePiArtifactVerifierArgs } from './verify-pi-product-artifact.mjs'; import { runProbe } from './probe-pi-runtime.mjs'; +import { runPackagedProductProof } from './run-pi-subagent-packaged-smoke.mjs'; -function runExtensionSmoke(projectRoot, artifact) { - return new Promise((resolvePromise, reject) => { - const child = spawn(process.execPath, [ - resolve(projectRoot, 'scripts', 'run-pi-subagent-packaged-smoke.mjs'), - '--runtime-root', artifact.artifact.runtimeRoot, - '--electron-executable', artifact.artifact.executable, - ], { - cwd: projectRoot, - stdio: 'inherit', - windowsHide: true, - }); - child.once('error', reject); - child.once('exit', (code, signal) => { - if (code === 0) resolvePromise({ result: 'pass' }); - else reject(new Error( - `Final product extension/subagent smoke failed with code ${code ?? 'null'} signal ${signal ?? 'none'}`, - )); - }); +async function runExtensionSmoke(projectRoot, artifact) { + return await runPackagedProductProof({ + projectRoot, + runtimeRoot: artifact.artifact.runtimeRoot, + electronExecutable: artifact.artifact.executable, + reportPath: undefined, }); } diff --git a/tests/unit/pi-managed-worker-opener.test.ts b/tests/unit/pi-managed-worker-opener.test.ts index 1111e74..7c7ea44 100644 --- a/tests/unit/pi-managed-worker-opener.test.ts +++ b/tests/unit/pi-managed-worker-opener.test.ts @@ -176,6 +176,8 @@ describe('managed Pi worker opener', () => { '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); expect(JSON.stringify(telemetry)).not.toContain(created.id); expect(JSON.stringify(telemetry)).not.toContain('PRIVATE MANAGED PROMPT'); expect(JSON.stringify(telemetry)).not.toContain('provider-secret-value'); diff --git a/tests/unit/pi-product-artifact.test.ts b/tests/unit/pi-product-artifact.test.ts index 5dc78b7..58b3bf7 100644 --- a/tests/unit/pi-product-artifact.test.ts +++ b/tests/unit/pi-product-artifact.test.ts @@ -1,6 +1,7 @@ // @vitest-environment node import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -9,6 +10,7 @@ import { assertNodeEngineCompatible, classifyOpenCodeResourcePaths, collectAbsoluteManifestValues, + collectForbiddenAsarPaths, collectForbiddenResourcePaths, defaultProductExecutable, validatePiArtifactMetadata, @@ -17,6 +19,7 @@ import { parsePiArtifactVerifierArgs } from '../../scripts/verify-pi-product-art const roots: string[] = []; const PI_PACKAGE = '@earendil-works/pi-coding-agent'; +const { createPackage } = createRequire(import.meta.url)('@electron/asar'); afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); @@ -85,14 +88,46 @@ describe('final Pi product artifact verification', () => { expect(classifyOpenCodeResourcePaths([ 'app.asar.unpacked/resources/opencode-runtime', 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js', + 'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode-codex-responses.js', ])).toEqual({ productOwned: ['app.asar.unpacked/resources/opencode-runtime'], upstreamPiProvider: [ 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js', + 'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode-codex-responses.js', ], }); }); + it('enumerates OpenCode-named paths inside app.asar instead of scanning only physical resources', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-asar-paths-')); + roots.push(root); + const source = path.join(root, 'source'); + await mkdir(path.join(source, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers'), { + recursive: true, + }); + await mkdir(path.join(source, 'resources', 'opencode-runtime'), { recursive: true }); + await writeFile( + path.join(source, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'opencode.js'), + 'export {};', + ); + await writeFile(path.join(source, 'resources', 'opencode-runtime', 'legacy.js'), 'export {};'); + const archive = path.join(root, 'app.asar'); + await createPackage(source, archive); + + const inventory = collectForbiddenAsarPaths(archive); + expect(inventory.entryCount).toBeGreaterThan(2); + expect(inventory.matches).toEqual(expect.arrayContaining([ + 'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js', + 'app.asar/resources/opencode-runtime', + ])); + expect(classifyOpenCodeResourcePaths(inventory.matches)).toMatchObject({ + productOwned: expect.arrayContaining(['app.asar/resources/opencode-runtime']), + upstreamPiProvider: expect.arrayContaining([ + 'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js', + ]), + }); + }); + it('uses final unpacked-product paths and strictly parses verifier options', () => { expect(defaultProductExecutable('/repo', 'linux')) .toBe(path.resolve('/repo', 'release', 'linux-unpacked', 'niancode')); diff --git a/tests/unit/pi-subagent.test.ts b/tests/unit/pi-subagent.test.ts index 36d95e9..e0b0028 100644 --- a/tests/unit/pi-subagent.test.ts +++ b/tests/unit/pi-subagent.test.ts @@ -86,6 +86,12 @@ describe('Pi subagent scheduler', () => { await expect.poll(() => running).toBe(4); expect(maxRunning).toBe(4); expect(processBudget.activeCount).toBe(4); + expect(scheduler.getDiagnostics()).toEqual({ + activeChildPermits: 4, + waitingChildPermits: 4, + activeDispatches: 2, + activeParents: 2, + }); gate.resolve(); const [leftResult, rightResult] = await Promise.all([left, right]); expect(leftResult.details.tasks).toHaveLength(4); @@ -93,6 +99,12 @@ describe('Pi subagent scheduler', () => { expect(opened).toHaveLength(8); expect(maxRunning).toBe(4); expect(processBudget.activeCount).toBe(0); + expect(scheduler.getDiagnostics()).toEqual({ + activeChildPermits: 0, + waitingChildPermits: 0, + activeDispatches: 0, + activeParents: 0, + }); await scheduler.close(); }); diff --git a/tests/unit/pi-worker-pool.test.ts b/tests/unit/pi-worker-pool.test.ts index 17f633f..42cf06f 100644 --- a/tests/unit/pi-worker-pool.test.ts +++ b/tests/unit/pi-worker-pool.test.ts @@ -714,7 +714,7 @@ describe('Pi worker pool', () => { expect(pool.getState('conversation-b')).toMatchObject({ state: 'ready', generation: 1 }); }); - it('records privacy-safe queue wait and RPC prompt acceptance spans', async () => { + it('records privacy-safe queue, acceptance, and authoritative settled spans', async () => { let now = 0; const telemetry: PiRuntimeTelemetryEvent[] = []; const workers = new Map(); @@ -757,10 +757,14 @@ describe('Pi worker pool', () => { expect(telemetry.map(({ milestone }) => milestone)).toEqual([ 'prompt.accepted', + 'agent.settled', 'worker.queue_wait', 'prompt.accepted', ]); - expect(telemetry[1]).toMatchObject({ durationMs: 20, workerGeneration: 1, cold: true }); + expect(telemetry[1]).toMatchObject({ + milestone: 'agent.settled', durationMs: 20, workerGeneration: 1, cold: true, + }); + expect(telemetry[2]).toMatchObject({ durationMs: 20, workerGeneration: 1, cold: true }); const serialized = JSON.stringify(telemetry); expect(serialized).not.toContain('private first prompt'); expect(serialized).not.toContain('private second prompt');