fix(ptc): simplify evaluation settlement and isolate published bundles
This commit is contained in:
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'] },
|
||||
])
|
||||
Reference in new issue
Block a user