fix(ptc): simplify evaluation settlement and isolate published bundles

This commit is contained in:
creatixchu committed 2026-09-23 17:03:04 +08:00
1 parent 7391d06a8d
commit 1393ed2e12
5 files changed
+41 -16

No files matched your search

@@ -78,7 +78,6 @@ export async function runPersistentMain(stream: Duplex, maxMessageBytes: number,
const evaluation: Evaluation = {
id, logs, active: true,
finish(result) {
if (!evaluation.active) return
evaluation.active = false
for (const call of pending.values()) call.reject(new Error('PTC evaluation ended before the binding completed'))
pending.clear()
@@ -109,8 +108,10 @@ export async function runPersistentMain(stream: Duplex, maxMessageBytes: number,
})
}, () => { channel.close(); closed.resolve() })
stream.once('close', () => { closed.resolve() })
await channel.send({ type: 'ready' })
try { await closed.promise } finally {
try {
await channel.send({ type: 'ready' })
await closed.promise
} finally {
if (current) current.active = false
repl.close()
input.destroy()
@@ -52,7 +52,6 @@ class Kernel {
const output = new OutputLedger(this.config.maxOutputBytes)
const logs: string[] = []
const result = Promise.withResolvers<PtcRunResult>()
let settled = false
const timer = spec.timeoutMs === null ? undefined : setTimeout(() => {
evaluation.finish({ kind: 'timeout', message: `execution deadline reached (${spec.timeoutMs}ms); retained variables were cleared` }, undefined, true)
}, spec.timeoutMs)
@@ -70,8 +69,6 @@ class Kernel {
}
},
finish: (error, value, discard = false) => {
if (settled) return
settled = true
clearTimeout(timer)
spec.signal?.removeEventListener('abort', onAbort)
this.active = undefined
@@ -139,7 +136,7 @@ class Kernel {
stream.on('error', (error: Error) => this.fail('worker-exit', error.message))
}
this.channel = new JsonChannel(handle.control, this.config.maxMessageBytes, (raw, bytes) => this.receive(raw, bytes), (error, kind) => {
if (!this.closed) this.fail(kind === 'protocol' ? 'protocol' : 'worker-exit', kind === 'protocol' ? error.message : 'persistent Node process stopped')
this.fail(kind === 'protocol' ? 'protocol' : 'worker-exit', kind === 'protocol' ? error.message : 'persistent Node process stopped')
})
void handle.done.then(() => {
setImmediate(() => { if (!this.closed) this.fail('worker-exit', 'persistent Node process stopped') })
@@ -147,16 +144,16 @@ class Kernel {
}
private receive(raw: unknown, bytes: number): void {
if (this.closed) return
if (!record(raw)) { this.fail('protocol', 'invalid persistent PTC frame'); return }
const evaluation = this.active
if (!evaluation) { this.fail('protocol', 'frame arrived without an active PTC evaluation'); return }
if (!this.ready) {
if (raw.type !== 'ready') { this.fail('protocol', 'expected persistent PTC readiness'); return }
this.ready = true
if (this.active) this.dispatch(this.active)
this.dispatch(evaluation)
return
}
const evaluation = this.active
if (!evaluation || raw.id !== evaluation.id || !record(raw.payload)) { this.fail('protocol', 'frame does not belong to the active PTC evaluation'); return }
if (raw.id !== evaluation.id || !record(raw.payload)) { this.fail('protocol', 'frame does not belong to the active PTC evaluation'); return }
const payload = raw.payload
switch (payload.type) {
case 'log':
@@ -138,7 +138,10 @@ describe('persistent PTC host protocol', () => {
})
it('returns current binding successes, exceptions and unavailable bindings', async () => {
const cases: Record<string, PtcBindingFunction>[] = [{ test: async () => 42 }, { test: async () => { throw new Error('denied') } }, {}]
const cases: Record<string, PtcBindingFunction>[] = [
{ test: async () => 42 }, { test: async () => { throw new Error('denied') } },
{ test: () => Promise.reject('denied') }, {},
]
for (const functions of cases) {
const h = await setup()
h.receive((raw) => {
@@ -167,7 +170,7 @@ describe('persistent PTC host protocol', () => {
it('cleans up after launch failure and cancellation during executable resolution', async () => {
const h = await setup()
h.resolveExecutable.mockRejectedValue(new Error('missing executable'))
h.resolveExecutable.mockRejectedValue('missing executable')
expect((await h.start(request, NO_INITIAL_FRAME)).error?.message).toContain('missing executable')
const waiting = await setup()
const ready = Promise.withResolvers<string>()
@@ -204,6 +207,7 @@ describe('persistent PTC host protocol', () => {
it('rejects invalid resolved inputs and deployment bounds', async () => {
const h = await setup()
expect(h.runtime.executionInstructions).toContain('Variables, functions, and imported modules remain in memory')
await expect(h.runtime.run({ ...request, cwd: '.', timeoutMs: null })).rejects.toThrow('resolved inputs')
await expect(h.runtime.run({ ...request, cwd: process.cwd(), timeoutMs: Infinity })).rejects.toThrow('resolved inputs')
for (const config of [
@@ -214,3 +218,18 @@ describe('persistent PTC host protocol', () => {
}
})
})
it('rejects corrupt framed traffic before it reaches the evaluator', async () => {
const h = await setup()
h.onBoot(() => { h.control.emit('data', Buffer.from([0, 0, 0, 1, 255])) })
expect((await h.start()).error?.kind).toBe('protocol')
})
it('discards unsolicited idle traffic and awaits the retained process exit', async () => {
const h = await setup()
h.onBoot(() => { h.emit(frame({ type: 'done' })) })
expect((await h.start()).error).toBeUndefined()
h.emit(frame({ type: 'log', text: 'late' }))
await h.direct.promise
expect(h.terminate).toHaveBeenCalledTimes(1)
})
@@ -103,3 +103,9 @@ it.each([0, 1.5, 4294967296])('rejects an invalid control limit %s', async (limi
await expect(runPersistentMain(stream, limit, { env: {}, stdout: { write: () => true }, stderr: { write: () => true }, chdir: () => {} })).rejects.toThrow('invalid control message limit')
} finally { stream.destroy() }
})
it('shuts down an idle REPL without creating an evaluation', async () => {
const h = await setup()
await h.close()
expect(h.seen).toEqual([])
})
@@ -1,7 +1,9 @@
import { defineConfig } from 'tsdown'
/** Separate entry bundles keep the private process bootstrap self-contained. */
/** Each public provider and private bootstrap is a self-contained published bundle. */
export default defineConfig([
{ entry: ['lib/types/index.js', 'lib/types/persistent.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false },
{ entry: { process: 'lib/types/process-entry.js', 'persistent-process': 'lib/types/persistent-process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control'] },
{ entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false },
{ entry: ['lib/types/persistent.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false },
{ entry: { process: 'lib/types/process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control'] },
{ entry: { 'persistent-process': 'lib/types/persistent-process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control'] },
])