fix(coding-agent): treat startup metadata as a non-conversation new session

The plan.defaultOnStartup gate keyed off getEntries().length === 0, but
createAgentSession appends startup metadata (model_change / thinking_level_change
/ service_tier_change) for a brand-new session before InteractiveMode.init()
runs. So a genuinely new session reached the guard with entries already present
and never entered plan mode (the unit harness bypassed the SDK path, hiding it).

Gate on the absence of conversation messages (entry.type === "message") instead.
A fresh session — including a --continue/auto-resume that created one — carries
only metadata and still honors the default; a session with restored conversation
keeps its reconciled mode. Added a regression test seeding model/thinking-level
metadata.
This commit is contained in:
ephraimduncan
2026-06-05 11:22:49 +00:00
parent df114ae042
commit d98bedd8fc
2 changed files with 33 additions and 15 deletions
@@ -116,11 +116,11 @@ describe("InteractiveMode plan.defaultOnStartup", () => {
expect(session?.getPlanModeState()).toBeUndefined();
});
it("does not enter plan mode when the session has restored history", async () => {
// A genuinely resumed session has prior entries; gating on getEntries()
// (not the CLI resume flag) means a `--continue` that created a *fresh*,
// empty session still gets the startup default (the "enters" case above),
// while one with restored history is left in its reconciled mode.
it("does not enter plan mode when the session has restored conversation", async () => {
// A genuinely resumed session has prior conversation messages. Gating on
// message entries (not the CLI resume flag) means a `--continue` that
// created a *fresh* session still gets the startup default (above), while
// one with restored conversation is left in its reconciled mode.
const created = createHarness(Settings.isolated({ "plan.defaultOnStartup": true, "compaction.enabled": false }));
created.sessionManager.appendMessage({ role: "user", content: "prior turn", timestamp: Date.now() });
@@ -130,6 +130,21 @@ describe("InteractiveMode plan.defaultOnStartup", () => {
expect(session?.getPlanModeState()).toBeUndefined();
});
it("enters plan mode for a fresh session that carries only startup metadata", async () => {
// createAgentSession appends model_change / thinking_level_change for a
// brand-new session before init(); those are not conversation history, so
// the startup default must still apply (regression: gating on entry count
// instead of message entries skipped plan mode for every real new session).
const created = createHarness(Settings.isolated({ "plan.defaultOnStartup": true, "compaction.enabled": false }));
created.sessionManager.appendModelChange("anthropic/claude-sonnet-4-5");
created.sessionManager.appendThinkingLevelChange("medium");
await created.init({ suppressWelcomeIntro: true });
expect(created.planModeEnabled).toBe(true);
expect(session?.getPlanModeState()).toMatchObject({ enabled: true });
});
it("does not enter plan mode when plan mode is globally disabled", async () => {
const created = createHarness(
Settings.isolated({ "plan.defaultOnStartup": true, "plan.enabled": false, "compaction.enabled": false }),