feat: add Forever Mode with time-based scheduled work and A2A listener

Add --forever CLI flag that enables autonomous agent operation with
scheduled work, context management, A2A protocol support, and session
optimization.

Core features:
- Time-based WorkScheduler: manages a sorted list of scheduled prompts
  that fire at absolute or relative times, persisted across sessions
- schedule_work tool: add/cancel scheduled prompts with 'at' (local
  time) or 'inMinutes' (relative) params; current time and schedule
  prepended to every model turn
- A2A HTTP listener: JSON-RPC 2.0 server bridges external messages
  into the session (message/send, tasks/get, responses/poll)
- PreCompress hook: hooks can return newHistory to replace built-in
  LLM compression
- Idle hook: fires after configurable inactivity, can auto-submit
  prompts
- Forever mode disables MemoryTool, EnterPlanModeTool, interactive
  shell

UI:
- ScheduledWorkDisplay component shows all pending items above the
  context summary bar
- A2A port shown in StatusDisplay when active

Session optimization for long-running sessions:
- Record lastCompressionIndex on ConversationRecord; on resume, only
  load post-compression messages
- Restore scheduled work items on session resume (past-due fire
  immediately)
- Skip file I/O in updateMessagesFromHistory when no tool results to
  sync
- Prune UI history to last 50 items after each context compression
This commit is contained in:
Sandy Tao
2026-03-17 21:34:50 -07:00
parent a5a461c234
commit 1a6f730439
51 changed files with 2737 additions and 55 deletions
@@ -52,6 +52,7 @@ const createMockUIState = (overrides: UIStateOverrides = {}): UIState =>
geminiMdFileCount: 0,
contextFileNames: [],
backgroundShellCount: 0,
a2aListenerPort: null,
buffer: { text: '' },
history: [{ id: 1, type: 'user', text: 'test' }],
...overrides,
@@ -171,4 +172,16 @@ describe('StatusDisplay', () => {
expect(lastFrame()).toContain('Shells: 3');
unmount();
});
it('renders A2A listener port when a2aListenerPort is set', async () => {
const uiState = createMockUIState({
a2aListenerPort: 8080,
});
const { lastFrame, unmount } = await renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toContain('A2A :8080');
unmount();
});
});