fix(coding-agent): persist user input source on user message entries

Merged
CO
2d ago
+385-11

Summary

A persisted user message now records where its input came from. inputSource is interactive, rpc or extension, the same value the input's input event reported (source ?? "interactive").

Part of code-yeongyu/oh-my-openagent#10378 (item 2b). omo's plan gate (plan-consultant / plan-reviewer) keeps per-session state built from input events. After a restart or restore that state is gone, and the transcript cannot rebuild the user-request channel because it does not say whether a user entry was typed or injected by an extension. The omo side replays the transcript and trusts only entries that carry this field.

What changed

  • agent-session.ts: the user message prompt() builds and the one _enqueuePreparedInput builds for queued steer/follow-up input carry inputSource. That object is the one message_end persists, so the session entry keeps it, the same way clientMessageId already does.
  • sendUserMessage's fallback enqueue after a rejected prompt() now passes source: "extension". Before, it defaulted to interactive.
  • Input restored from durable custody (restoreQueuedInput) or requeued by a session move ran no input event and stays unstamped. Consumers must treat an absent value as unknown origin, never as typed input; docs/session-format.md says so.
  • core/user-input-source.ts (new helper), core/changes.md entry, CHANGELOG Added entry.

Evidence (head ed36d3d6d)

  • RED: test/suite/regressions/omo-10378-user-input-source.test.ts failed 4/4 before the fix, each with inputSource: undefined (typed prompt, rpc prompt, idle sendUserMessage, and queued steer/typed follow-up/injected follow-up).
  • GREEN: that file plus agent-session-queue, pending-input-session and extension-edit-user-message: 4 files, 86/86 passed.
  • bun run check: exit 0.
  • Real CLI (senpi-qa harness, --mode rpc with session persistence, mock openai-completions server): two RPC prompts, and the session JSONL's user entries read back [{"text":"first rpc prompt","inputSource":"rpc"},{"text":"second rpc prompt","inputSource":"rpc"}]. The real auth file was unchanged (harness guard). Evidence held locally:
44e0f69d5581d53f5ce065731ca16341c2bc4bcd4829bfc1be56cd78a84467bc  qa-driver.mjs
f4447001dda108049b2ac75dac226aaeecf6dc9a7334747940f343e730595f91  persisted-user-entries.json
  • Full packages/coding-agent suite: running; result to follow in a comment.

Summary by cubic

Persisted user messages now record where their input came from so a resumed session's transcript can rebuild per-session trust. inputSource on the message session entry is interactive, rpc, or extension, matching the input event's source ?? "interactive". Part of code-yeongyu/oh-my-openagent#10378, where the plan gate must tell typed input apart from extension-injected input after a restart.

  • The source lives on the session entry only and never reaches a provider request.
  • Queued input restored after a host restart keeps its recorded source, including a prompt queued as a steer after losing the start race; input requeued by a session move, edited input, and pre-change records carry no inputSource and must be treated as unknown origin (documented in docs/session-format.md).
  • sendUserMessage's fallback enqueue after a rejected prompt() now passes source: "extension" instead of defaulting to interactive.