fix: allow lineage-bearing stdio targets
This commit is contained in:
@@ -95,20 +95,18 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
|
|||||||
const welcome = config.welcome ?? 'ready.'
|
const welcome = config.welcome ?? 'ready.'
|
||||||
const { input, output, exit } = runtime
|
const { input, output, exit } = runtime
|
||||||
|
|
||||||
// This app owns one root agent. Hold the live object directly: its per-run id
|
// This app owns one configured agent. Hold the live object directly: its
|
||||||
// is intentionally fresh, while `main` remains only the terminal's fixed
|
// per-run id is intentionally fresh, while `main` remains only the
|
||||||
// display label. HMR may publish the replacement before old teardown emits
|
// terminal's fixed display label. At install the configured agent is the
|
||||||
// disposed, so a target disposal reselects the surviving root from the live
|
// earliest registry entry (it creates any subagents later). During HMR the
|
||||||
// registry instead of leaving the terminal detached. Fork children carry
|
// replacement is published after the old tree, so when old teardown finally
|
||||||
// parentSession lineage and must never become the terminal target.
|
// emits disposed, the newest survivor is the replacement. Persisted
|
||||||
const rootAgent = (): Agent | undefined =>
|
// parentSession lineage is deliberately irrelevant: a resumed child session
|
||||||
ctx.agents.list().find(agent => agent.session.header.parentSession === undefined)
|
// can itself be this process's configured top-level agent.
|
||||||
let target: Agent | undefined = rootAgent()
|
let target: Agent | undefined = ctx.agents.list()[0]
|
||||||
ctx.on('agent/created', (agent) => {
|
ctx.on('agent/created', (agent) => { target ??= agent })
|
||||||
if (target === undefined && agent.session.header.parentSession === undefined) target = agent
|
|
||||||
})
|
|
||||||
ctx.on('agent/disposed', (agent) => {
|
ctx.on('agent/disposed', (agent) => {
|
||||||
if (target === agent) target = rootAgent()
|
if (target === agent) target = ctx.agents.list().at(-1)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Transcript rendering off the durable `session/event` feed — the assistant
|
// Transcript rendering off the durable `session/event` feed — the assistant
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ describe('createStdioChat rendering', () => {
|
|||||||
await ctx.plugin(AgentRegistry)
|
await ctx.plugin(AgentRegistry)
|
||||||
await ctx.plugin(UserInteractionService)
|
await ctx.plugin(UserInteractionService)
|
||||||
const agent = makeAgent('main')
|
const agent = makeAgent('main')
|
||||||
|
// Durable lineage does not imply runtime child ownership: the stdio app
|
||||||
|
// may explicitly resume a persisted fork as its one configured agent.
|
||||||
|
;(agent.session.header as { parentSession?: string }).parentSession = 'persisted-parent'
|
||||||
ctx.agents.register(agent) // registered BEFORE the UI plugin below
|
ctx.agents.register(agent) // registered BEFORE the UI plugin below
|
||||||
const { runtime, out } = makeRuntime()
|
const { runtime, out } = makeRuntime()
|
||||||
await ctx.plugin(Object.assign((inner: Context) => {
|
await ctx.plugin(Object.assign((inner: Context) => {
|
||||||
@@ -196,6 +199,18 @@ describe('createStdioChat rendering', () => {
|
|||||||
expect(out.text()).toContain('[main turn 5] ')
|
expect(out.text()).toContain('[main turn 5] ')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('accepts a lineage-bearing configured agent created after the UI installs', async () => {
|
||||||
|
const { ctx, input } = await setup()
|
||||||
|
const resumed = makeAgent('resumed')
|
||||||
|
;(resumed.session.header as { parentSession?: string }).parentSession = 'persisted-parent'
|
||||||
|
ctx.emit('agent/created', resumed)
|
||||||
|
|
||||||
|
input.feed('continue')
|
||||||
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
|
|
||||||
|
expect(resumed.sent).toEqual([[{ type: 'text', text: 'continue' }]])
|
||||||
|
})
|
||||||
|
|
||||||
it('resets dim styling at turn/end if a turn ends mid-reasoning', async () => {
|
it('resets dim styling at turn/end if a turn ends mid-reasoning', async () => {
|
||||||
const { ctx, out } = await setup()
|
const { ctx, out } = await setup()
|
||||||
const session = makeSession('main')
|
const session = makeSession('main')
|
||||||
@@ -242,7 +257,7 @@ describe('createStdioChat rendering', () => {
|
|||||||
|
|
||||||
// The replacement's created edge arrived while oldRoot was still targeted.
|
// The replacement's created edge arrived while oldRoot was still targeted.
|
||||||
// Once oldRoot is removed, registry order is child then replacement; the
|
// Once oldRoot is removed, registry order is child then replacement; the
|
||||||
// UI must skip the surviving child and route input to the replacement root.
|
// most recently published survivor is the HMR replacement.
|
||||||
disposeOld()
|
disposeOld()
|
||||||
input.feed('after hmr')
|
input.feed('after hmr')
|
||||||
await new Promise(resolve => setImmediate(resolve))
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
|
|||||||
Reference in New Issue
Block a user