fix(plan): commit an idle selection immediately

set() on an idle agent appends plan/mode at once — no request boundary
would arrive until the next prompt, so a queued intent used to hang as
pending forever (the composer showed a dead pending target). A running
agent keeps the boundary-flush path unchanged. set() now reports which
branch ran (committed/queued/cancelled/noop); the /plan handler's copy
follows the branch (idle: "Plan mode on/off", mid-turn: the next-step
wording), and both commit paths share the header-delta narration. The
invariant drops turn enclosure: plan/mode is a standalone whole-value
event (the synthetic log-only turns removal already established the
between-turns append shape). The fixture mirrors the idle commit.
This commit is contained in:
imccyu
2026-07-28 23:09:15 +08:00
parent afaa9ad828
commit 970b432227
8 changed files with 123 additions and 59 deletions
@@ -44,10 +44,10 @@ describe('plan-mode stream invariants', () => {
.toThrow(/expected a boolean/)
})
it('rejects plan state outside any open turn', async () => {
it('accepts standalone plan state between turns (the idle immediate commit)', async () => {
const ctx = await setup()
expect(() => ctx.sessions.create().append('plan/mode', { active: true }))
.toThrow(/outside any open turn/)
.not.toThrow()
})
it('ignores unrelated dispatches and session events', async () => {
@@ -85,12 +85,12 @@ describe('plan-mode stream invariants', () => {
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
})
it('rejects unenclosed existing plan state on late registration', async () => {
it('accepts standalone existing plan state on late registration', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
ctx.sessions.create().append('plan/mode', { active: true })
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/outside any open turn/)
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
})
})