fix(agent-loop): open turns before pre-step
This commit is contained in:
@@ -108,7 +108,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
* Run and fold one configured Codex hook point.
|
||||
*
|
||||
* A supplied turn records the hook provenance pair inside that open turn.
|
||||
* Pre-turn `UserPromptSubmit` and detached lifecycle points omit it.
|
||||
* Detached lifecycle points omit it.
|
||||
*/
|
||||
async function runPoint(
|
||||
point: string,
|
||||
@@ -203,7 +203,9 @@ export function apply(ctx: Context, config: Config): void {
|
||||
turn_id: String(turn),
|
||||
prompt: blocksToText(messages.flatMap(message => message.content)),
|
||||
}
|
||||
const merged = await runPoint('UserPromptSubmit', '', payload, { agent, plainStdoutAsContext: true, signal })
|
||||
const merged = await runPoint('UserPromptSubmit', '', payload, {
|
||||
agent, turn, plainStdoutAsContext: true, signal,
|
||||
})
|
||||
/* jscpd:ignore-start */
|
||||
if (merged.decision === 'deny') {
|
||||
return { kind: 'reject' }
|
||||
|
||||
@@ -125,8 +125,9 @@ describe('hooks-codex bridge', () => {
|
||||
|
||||
expect(() => process.kill(pid, 0)).toThrow()
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(events(agent).some(event => event.type === 'turn/start')).toBe(false)
|
||||
expect(events(agent).some(event => event.type === 'hook/invoked' || event.type === 'hook/result')).toBe(false)
|
||||
expect(events(agent).filter(event => event.type === 'turn/start' || event.type === 'hook/invoked'
|
||||
|| event.type === 'hook/result' || event.type === 'turn/end').map(event => event.type))
|
||||
.toEqual(['turn/start', 'hook/invoked', 'hook/result', 'turn/end'])
|
||||
})
|
||||
|
||||
it('only the five bridge-supported Codex events are honored — a SubagentStop entry is ignored', async () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
expect((await capture()).payload.transcript_path).toBeNull()
|
||||
}, 15_000) // Two real agent/hook subprocess loops need process startup and teardown headroom.
|
||||
|
||||
it('UserPromptSubmit block (exit 2) rejects step entry without a turn', async () => {
|
||||
it('UserPromptSubmit block (exit 2) closes a blocked turn without a step', async () => {
|
||||
const d = dir()
|
||||
hooks(d, { UserPromptSubmit: [{ hooks: [{ type: 'command', command: sh(d, 'b.sh', '#!/usr/bin/env bash\nexit 2\n') }] }] })
|
||||
const adapter = new MockAdapter([textResponse('no')])
|
||||
@@ -89,7 +89,9 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })); await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(events(agent).some(e => e.type === 'turn/start')).toBe(false)
|
||||
expect(events(agent).filter(e => e.type === 'turn/start' || e.type === 'hook/invoked'
|
||||
|| e.type === 'hook/result' || e.type === 'turn/end').map(e => e.type))
|
||||
.toEqual(['turn/start', 'hook/invoked', 'hook/result', 'turn/end'])
|
||||
})
|
||||
|
||||
it('UserPromptSubmit additionalContext is injected; a no-op hook proceeds', async () => {
|
||||
@@ -116,7 +118,9 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })); await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(events(agent).some(e => e.type === 'user/message')).toBe(false)
|
||||
expect(events(agent).some(e => e.type === 'turn/start')).toBe(false)
|
||||
expect(events(agent).filter(e => e.type === 'turn/start' || e.type === 'hook/invoked'
|
||||
|| e.type === 'hook/result' || e.type === 'turn/end').map(e => e.type))
|
||||
.toEqual(['turn/start', 'hook/invoked', 'hook/result', 'turn/end'])
|
||||
})
|
||||
|
||||
it('preserves separate bridge and downstream prompt contexts with framing and metadata', async () => {
|
||||
|
||||
Reference in New Issue
Block a user