feat: agent-to-agent integrations — activity stream, steer channel, plan gate

Three fork-side integrations that turn OpenChamber from a fire-and-forget
coder into a visible, steerable, plan-gated agent:

1. Activity stream (GET /api/openchamber/agent-activity SSE)
   - agent-activity/runtime.js: subscribes to global hub, normalizes
     message.updated parts into structured activity events (tool-call,
     file-edit, text-part), rate-limited coalescing for tool calls
   - Broadcasts openchamber:agent-activity and session-completed events
   - SSE endpoint with heartbeat (25s), same shape as /api/openchamber/events

2. Steer channel (POST /api/openchamber/session/:id/steer)
   - session-steer/runtime.js: interrupt mode (interrupt → inject
     system-level directive → resume) and queue mode (deliver on next idle)
   - Follows message-queue precedent for interrupt-safe dispatch
   - Broadcasts openchamber:steer-delivered on queued delivery

3. Plan-first gate (plan-gate/runtime.js + approve/reject/status routes)
   - State machine per session: pending → approved | rejected | timed_out
   - Injects plan-gate reminder via openchamber-sessions create prompt
   - Agent emits ## Plan, runtime detects and emits openchamber:plan-ready
   - Approve sends 'Proceed' prompt, reject sends revision prompt
   - Configurable timeout (default 5 min), auto-approve on timeout

4. P1 shared plumbing
   - cardID accepted in session create payload, stored in session metadata
   - All new events carry cardID when known

Files added:
- packages/web/server/lib/agent-activity/runtime.js + runtime.test.js
- packages/web/server/lib/session-steer/runtime.js + runtime.test.js
- packages/web/server/lib/plan-gate/runtime.js + runtime.test.js

Files modified:
- packages/web/server/lib/openchamber-sessions/routes.js (cardID, planGate)
- packages/web/server/lib/opencode/feature-routes-runtime.js (route wiring)
- packages/web/server/index.js (runtime creation, SSE_PATH_PREFIXES)
- packages/web/server/lib/ui-auth/ui-auth.js (auth allowlist)
- packages/web/server/lib/realtime-proxy.js (SSE allowlist)

28 new tests passing. All pre-existing tests unaffected.
This commit is contained in:
2026-09-07 22:34:56 +00:00
parent 4b95b327dd
commit 05d8e953ca
11 changed files with 1488 additions and 1 deletions
+29
View File
@@ -92,6 +92,9 @@ import { createApnsRuntime } from './lib/notifications/apns-runtime.js';
import { createNotificationTemplateRuntime } from './lib/notifications/template-runtime.js';
import { createPermissionAutoAcceptRuntime } from './lib/permission-auto-accept/runtime.js';
import { createMessageQueueRuntime } from './lib/message-queue/runtime.js';
import { createAgentActivityRuntime, registerAgentActivityRoutes } from './lib/agent-activity/runtime.js';
import { createSessionSteerRuntime, registerSessionSteerRoutes } from './lib/session-steer/runtime.js';
import { createPlanGateRuntime, registerPlanGateRoutes } from './lib/plan-gate/runtime.js';
import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
import { createProjectConfigRuntime } from './lib/projects/project-config.js';
import { createProjectContextRuntime } from './lib/project-context/runtime.js';
@@ -167,6 +170,7 @@ const SSE_PATH_PREFIXES = [
'/api/notifications/stream',
'/api/openchamber/events',
'/api/openchamber/realtime-proxy/sse',
'/api/openchamber/agent-activity',
];
function shouldSkipCompression(req, res) {
@@ -899,6 +903,28 @@ const messageQueueRuntime = createMessageQueueRuntime({
});
messageQueueRuntime.start();
const agentActivityRuntime = createAgentActivityRuntime({
globalEventHub: globalMessageStreamHub,
broadcastGlobalUiEvent,
});
agentActivityRuntime.start();
const sessionSteerRuntime = createSessionSteerRuntime({
globalEventHub: globalMessageStreamHub,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
broadcastGlobalUiEvent,
});
sessionSteerRuntime.start();
const planGateRuntime = createPlanGateRuntime({
globalEventHub: globalMessageStreamHub,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
broadcastGlobalUiEvent,
});
planGateRuntime.start();
const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
waitForOpenCodePort: (...args) => waitForOpenCodePort(...args),
buildOpenCodeUrl,
@@ -1947,6 +1973,9 @@ async function main(options = {}) {
writeSseEvent,
permissionAutoAcceptRuntime,
messageQueueRuntime,
agentActivityRuntime,
sessionSteerRuntime,
planGateRuntime,
});
const startupPipelineResult = await startupPipelineRuntime.run({