feat(companion): add bootstrap manifest export for app packaging
This commit is contained in:
@@ -87,7 +87,7 @@ Flynn provides a full CLI via the `flynn` binary (or `npx tsx src/cli/index.ts`
|
|||||||
| `flynn google-auth --service <name>` | Unified Google OAuth entrypoint (`gmail`, `gcal`, `gdocs`, `gdrive`, `gtasks`) |
|
| `flynn google-auth --service <name>` | Unified Google OAuth entrypoint (`gmail`, `gcal`, `gdocs`, `gdrive`, `gtasks`) |
|
||||||
| `flynn gemini-auth` | Store a Gemini API key in `~/.config/flynn/auth.json` |
|
| `flynn gemini-auth` | Store a Gemini API key in `~/.config/flynn/auth.json` |
|
||||||
| `flynn skills` | List/install/manage skills |
|
| `flynn skills` | List/install/manage skills |
|
||||||
| `flynn companion` | Run a minimal companion node client against the gateway |
|
| `flynn companion` | Run a minimal companion node client or export a companion bootstrap manifest |
|
||||||
|
|
||||||
`flynn setup` / `flynn onboard` now include:
|
`flynn setup` / `flynn onboard` now include:
|
||||||
- a **Personal Assistant Mode** first-run preset (announce delivery, proactive memory, talk mode defaults, TTS fallback policy),
|
- a **Personal Assistant Mode** first-run preset (announce delivery, proactive memory, talk mode defaults, TTS fallback policy),
|
||||||
@@ -1734,11 +1734,13 @@ Companion runtime helper:
|
|||||||
- stream passthrough helpers (`subscribeEvents`, `subscribeEvent`, `clearEventSubscriptions`, `cancelPendingEventWaits`, `listKnownEventNames`, `eventSubscriptionCount`, `subscribeAgentStream/Typing/ContextWarning`, `waitForEvent`, `waitForAnyEvent`, `waitForAgentStream/Typing/ContextWarning`)
|
- stream passthrough helpers (`subscribeEvents`, `subscribeEvent`, `clearEventSubscriptions`, `cancelPendingEventWaits`, `listKnownEventNames`, `eventSubscriptionCount`, `subscribeAgentStream/Typing/ContextWarning`, `waitForEvent`, `waitForAnyEvent`, `waitForAgentStream/Typing/ContextWarning`)
|
||||||
- runtime observability/control passthroughs (`pendingRequestCount`, `pendingEventWaitCount`, `hasPendingWork`, `idle`, `lastDisconnectCode`, `lastDisconnectReason`, `getPendingWorkSnapshot()`, `getEventSurfaceSnapshot()`, `getConnectionSnapshot()`, `connected`, `waitForIdle()`)
|
- runtime observability/control passthroughs (`pendingRequestCount`, `pendingEventWaitCount`, `hasPendingWork`, `idle`, `lastDisconnectCode`, `lastDisconnectReason`, `getPendingWorkSnapshot()`, `getEventSurfaceSnapshot()`, `getConnectionSnapshot()`, `connected`, `waitForIdle()`)
|
||||||
- `src/companion/heartbeatLoop.ts` provides `CompanionHeartbeatLoop` for periodic heartbeat scheduling (`publishHeartbeat`) with start/stop safety, optional interval jitter (`jitterRatio`) to spread load (with safe normalization for invalid random samples), `tickNow()` for manual sends, success/error hooks, loop observability (`successCount`, `lastSuccessAt`, `failureCount`, `lastFailure`, `getState()`), and optional auto-stop after repeated failures.
|
- `src/companion/heartbeatLoop.ts` provides `CompanionHeartbeatLoop` for periodic heartbeat scheduling (`publishHeartbeat`) with start/stop safety, optional interval jitter (`jitterRatio`) to spread load (with safe normalization for invalid random samples), `tickNow()` for manual sends, success/error hooks, loop observability (`successCount`, `lastSuccessAt`, `failureCount`, `lastFailure`, `getState()`), and optional auto-stop after repeated failures.
|
||||||
|
- `src/companion/bootstrapManifest.ts` provides `createCompanionBootstrapManifest()` for generating a typed gateway/node/runtime bootstrap contract used by packaging flows.
|
||||||
|
|
||||||
Minimal companion CLI:
|
Minimal companion CLI:
|
||||||
- `flynn companion --once` connects to the gateway, registers a node, publishes one heartbeat, then exits.
|
- `flynn companion --once` connects to the gateway, registers a node, publishes one heartbeat, then exits.
|
||||||
- `flynn companion --platform macos --heartbeat 30` runs a long-lived node with periodic heartbeats and logs `agent.stream`/`agent.typing` events.
|
- `flynn companion --platform macos --heartbeat 30` runs a long-lived node with periodic heartbeats and logs `agent.stream`/`agent.typing` events.
|
||||||
- `flynn companion --once --handoff "summarize my status"` performs one post-registration `agent.send` handoff and prints the `done` content.
|
- `flynn companion --once --handoff "summarize my status"` performs one post-registration `agent.send` handoff and prints the `done` content.
|
||||||
|
- `flynn companion --export-bootstrap ./companion.bootstrap.json` writes a resolved bootstrap manifest for desktop/mobile companion app packaging (use `-` for stdout).
|
||||||
|
|
||||||
## WebChat PWA Push Subscriptions
|
## WebChat PWA Push Subscriptions
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ The gateway provides:
|
|||||||
- **HTTP Server**: Serves static dashboard and handles webhook endpoints
|
- **HTTP Server**: Serves static dashboard and handles webhook endpoints
|
||||||
- **Node Capability Negotiation**: Optional companion-node role/capability registration
|
- **Node Capability Negotiation**: Optional companion-node role/capability registration
|
||||||
|
|
||||||
Operational note: onboarding (`flynn setup` / `flynn onboard`) now runs post-save live readiness checks (model/channel/memory/automation) and prints a guided first-success task flow. This improves setup reliability without changing JSON-RPC method/event shapes.
|
Operational note: onboarding (`flynn setup` / `flynn onboard`) now runs post-save live readiness checks (model/channel/memory/automation) and prints a guided first-success task flow. Companion CLI now also supports bootstrap-manifest export (`flynn companion --export-bootstrap <path|->`) for desktop/mobile app packaging without changing JSON-RPC method/event shapes.
|
||||||
|
|
||||||
### Execution Model (Sessions + Per-Session Queue)
|
### Execution Model (Sessions + Per-Session Queue)
|
||||||
|
|
||||||
@@ -1862,3 +1862,4 @@ For more implementation details, see:
|
|||||||
- Gateway server: `src/gateway/server.ts`
|
- Gateway server: `src/gateway/server.ts`
|
||||||
- Companion runtime client helper: `src/companion/runtimeClient.ts` (node + system + `canvas.*` typed RPC wrappers, optional `autoConnect`/`autoReconnect`, optional reconnect state replay, `sendAgentMessage` handoff helper, connection event subscriptions)
|
- Companion runtime client helper: `src/companion/runtimeClient.ts` (node + system + `canvas.*` typed RPC wrappers, optional `autoConnect`/`autoReconnect`, optional reconnect state replay, `sendAgentMessage` handoff helper, connection event subscriptions)
|
||||||
- Platform companion wrappers: `src/companion/platformClients.ts`
|
- Platform companion wrappers: `src/companion/platformClients.ts`
|
||||||
|
- Companion bootstrap manifest helper: `src/companion/bootstrapManifest.ts` (typed packaging manifest contract used by `flynn companion --export-bootstrap`)
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ Gateway streaming UX signals:
|
|||||||
- WebSocket `agent.send` emits `run_state` lifecycle events (`start`, `cancel_requested`, `cancelled`, `complete`, `error`) for UI/state rendering.
|
- WebSocket `agent.send` emits `run_state` lifecycle events (`start`, `cancel_requested`, `cancelled`, `complete`, `error`) for UI/state rendering.
|
||||||
- Routing applies reaction rules with deterministic priority/cooldown (and recursion guard) before intent routing.
|
- Routing applies reaction rules with deterministic priority/cooldown (and recursion guard) before intent routing.
|
||||||
- Companion nodes re-register `node.*` capabilities after reconnect; runtime clients can auto-reconnect, optionally replay cached node state (`register/status/location/push`), and surface connection events.
|
- Companion nodes re-register `node.*` capabilities after reconnect; runtime clients can auto-reconnect, optionally replay cached node state (`register/status/location/push`), and surface connection events.
|
||||||
|
- `flynn companion --export-bootstrap <path|->` can emit a resolved companion bootstrap manifest (gateway/node/runtime contract) for desktop/mobile packaging flows without opening a runtime connection.
|
||||||
- Canvas artifacts are persisted by the gateway so session UI surfaces can recover after daemon restarts.
|
- Canvas artifacts are persisted by the gateway so session UI surfaces can recover after daemon restarts.
|
||||||
- TTS synthesis uses an ordered provider chain with health cooldown tracking; if all providers fail, replies degrade to text-only without dropping the response.
|
- TTS synthesis uses an ordered provider chain with health cooldown tracking; if all providers fail, replies degrade to text-only without dropping the response.
|
||||||
- Talk mode accepts spoken/text `stop`/`cancel` while active and maps it onto the same `/stop` run-control cancellation path used for text sessions.
|
- Talk mode accepts spoken/text `stop`/`cancel` while active and maps it onto the same `/stop` run-control cancellation path used for text sessions.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ If you only want the protocol surface, see `docs/api/PROTOCOL.md`.
|
|||||||
- `subagent.*` tools create child orchestrators scoped to the parent conversation (`subagent:<parentSessionId>:<childId>`) with idle TTL cleanup, per-child queue mode (`followup|interrupt`), and session budgets (turn/token/timeout); this is tool-loop behavior, not a separate gateway RPC session lane.
|
- `subagent.*` tools create child orchestrators scoped to the parent conversation (`subagent:<parentSessionId>:<childId>`) with idle TTL cleanup, per-child queue mode (`followup|interrupt`), and session budgets (turn/token/timeout); this is tool-loop behavior, not a separate gateway RPC session lane.
|
||||||
- Browser workflow reliability primitives (`browser.wait_for/assert/extract/checkpoint.*`) execute in the same queued session lane and apply browser-config guardrails (domain allowlist/high-risk confirmation, bounded retries, workflow step budget).
|
- Browser workflow reliability primitives (`browser.wait_for/assert/extract/checkpoint.*`) execute in the same queued session lane and apply browser-config guardrails (domain allowlist/high-risk confirmation, bounded retries, workflow step budget).
|
||||||
- Companion `node.*` registration is per WebSocket connection; reconnects must re-register capabilities before invoking node RPC methods (or use runtime-client reconnect state replay to re-register/status/location/push automatically).
|
- Companion `node.*` registration is per WebSocket connection; reconnects must re-register capabilities before invoking node RPC methods (or use runtime-client reconnect state replay to re-register/status/location/push automatically).
|
||||||
|
- Companion packaging/bootstrap can be generated offline via `flynn companion --export-bootstrap <path|->`, which emits resolved gateway/node/runtime settings without opening a WebSocket session.
|
||||||
- Canvas artifacts are persisted per session under the gateway data directory for UI recovery across restarts.
|
- Canvas artifacts are persisted per session under the gateway data directory for UI recovery across restarts.
|
||||||
- TTS output is best-effort with ordered provider fallback + per-provider cooldown tracking; synthesis failures still fall back to text-only responses.
|
- TTS output is best-effort with ordered provider fallback + per-provider cooldown tracking; synthesis failures still fall back to text-only responses.
|
||||||
- Talk mode voice sessions share the same cancel/replace semantics as text lanes (`/stop`, interrupt mode preemption), including spoken `stop`/`cancel` mapping while talk mode is active.
|
- Talk mode voice sessions share the same cancel/replace semantics as text lanes (`/stop`, interrupt mode preemption), including spoken `stop`/`cancel` mapping while talk mode is active.
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ Within 8-10 weeks, ship a stable "Personal Assistant Mode" that supports:
|
|||||||
2. Ship a minimal mobile companion shell (iOS + Android) for registration, status, push token, and message handoff.
|
2. Ship a minimal mobile companion shell (iOS + Android) for registration, status, push token, and message handoff.
|
||||||
3. Add signed release artifacts and installation docs.
|
3. Add signed release artifacts and installation docs.
|
||||||
|
|
||||||
|
Status update (2026-02-27): companion bootstrap-manifest export is now available via `flynn companion --export-bootstrap <path|->` as a packaging contract for desktop/mobile shells.
|
||||||
|
|
||||||
### Implementation Anchors
|
### Implementation Anchors
|
||||||
|
|
||||||
1. `src/companion/runtimeClient.ts`
|
1. `src/companion/runtimeClient.ts`
|
||||||
|
|||||||
+78
-58
@@ -3657,7 +3657,7 @@
|
|||||||
"multi_agent_routing": {
|
"multi_agent_routing": {
|
||||||
"priority": "P2",
|
"priority": "P2",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Config-driven agent routing: AgentConfigRegistry for named agent configs (system_prompt, model_tier, tool_profile, sandbox), AgentRouter with sender\u2192channel\u2192default resolution (glob patterns), per-agent tool registry cloning with sandboxed overrides, daemon wiring",
|
"description": "Config-driven agent routing: AgentConfigRegistry for named agent configs (system_prompt, model_tier, tool_profile, sandbox), AgentRouter with sender→channel→default resolution (glob patterns), per-agent tool registry cloning with sandboxed overrides, daemon wiring",
|
||||||
"files_created": [
|
"files_created": [
|
||||||
"src/agents/registry.ts",
|
"src/agents/registry.ts",
|
||||||
"src/agents/registry.test.ts",
|
"src/agents/registry.test.ts",
|
||||||
@@ -3811,7 +3811,7 @@
|
|||||||
"auto_login": {
|
"auto_login": {
|
||||||
"priority": "P5",
|
"priority": "P5",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Lazy token resolution with onLoginRequired callback \u2014 triggers OAuth device flow automatically on first API call when no token is available",
|
"description": "Lazy token resolution with onLoginRequired callback — triggers OAuth device flow automatically on first API call when no token is available",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/models/github.ts",
|
"src/models/github.ts",
|
||||||
"src/daemon/index.ts",
|
"src/daemon/index.ts",
|
||||||
@@ -3959,7 +3959,7 @@
|
|||||||
"p8-agent-tools": {
|
"p8-agent-tools": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-07",
|
"date": "2026-02-07",
|
||||||
"summary": "8 new agent-callable tools exposing existing internal APIs, plus gap analysis audit update (25% \u2192 65% match rate)",
|
"summary": "8 new agent-callable tools exposing existing internal APIs, plus gap analysis audit update (25% → 65% match rate)",
|
||||||
"phases": {
|
"phases": {
|
||||||
"sessions_tools": {
|
"sessions_tools": {
|
||||||
"priority": "P8",
|
"priority": "P8",
|
||||||
@@ -4016,7 +4016,7 @@
|
|||||||
"file_patch_tool": {
|
"file_patch_tool": {
|
||||||
"priority": "Tier3",
|
"priority": "Tier3",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "file.patch tool for multi-hunk structured patches \u2014 apply multiple line-based edits (replacements, insertions, deletions) across one or more files in a single tool call. Hunks applied bottom-up to preserve line numbers.",
|
"description": "file.patch tool for multi-hunk structured patches — apply multiple line-based edits (replacements, insertions, deletions) across one or more files in a single tool call. Hunks applied bottom-up to preserve line numbers.",
|
||||||
"files_created": [
|
"files_created": [
|
||||||
"src/tools/builtin/file-patch.ts",
|
"src/tools/builtin/file-patch.ts",
|
||||||
"src/tools/builtin/file-patch.test.ts"
|
"src/tools/builtin/file-patch.test.ts"
|
||||||
@@ -4030,7 +4030,7 @@
|
|||||||
"gmail_pubsub_watcher": {
|
"gmail_pubsub_watcher": {
|
||||||
"priority": "Tier3",
|
"priority": "Tier3",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Gmail Pub/Sub watcher ChannelAdapter \u2014 monitors Gmail via Google Cloud Pub/Sub push notifications with polling fallback. OAuth2 auth, configurable watch labels, template rendering with email metadata placeholders. Wired into daemon lifecycle and gateway (POST /gmail/push endpoint).",
|
"description": "Gmail Pub/Sub watcher ChannelAdapter — monitors Gmail via Google Cloud Pub/Sub push notifications with polling fallback. OAuth2 auth, configurable watch labels, template rendering with email metadata placeholders. Wired into daemon lifecycle and gateway (POST /gmail/push endpoint).",
|
||||||
"files_created": [
|
"files_created": [
|
||||||
"src/automation/gmail.ts",
|
"src/automation/gmail.ts",
|
||||||
"src/automation/gmail.test.ts"
|
"src/automation/gmail.test.ts"
|
||||||
@@ -4155,7 +4155,7 @@
|
|||||||
"phases": {
|
"phases": {
|
||||||
"ollama_tool_calling": {
|
"ollama_tool_calling": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Pass tools to Ollama API in correct format, parse tool_calls from responses with generated IDs, set stopReason to 'tool_use'. Handle thinking field from reasoning models (deepseek-r1, glm-4.7-flash) \u2014 use as content fallback and expose via thinkingContent. Streaming support for both tool calls and thinking.",
|
"description": "Pass tools to Ollama API in correct format, parse tool_calls from responses with generated IDs, set stopReason to 'tool_use'. Handle thinking field from reasoning models (deepseek-r1, glm-4.7-flash) — use as content fallback and expose via thinkingContent. Streaming support for both tool calls and thinking.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/models/local/ollama.ts",
|
"src/models/local/ollama.ts",
|
||||||
"src/models/local/ollama.test.ts"
|
"src/models/local/ollama.test.ts"
|
||||||
@@ -4181,7 +4181,7 @@
|
|||||||
"lane_queue": {
|
"lane_queue": {
|
||||||
"priority": "Tier3",
|
"priority": "Tier3",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Per-session FIFO queue in gateway \u2014 serializes concurrent requests instead of rejecting. LaneQueue class with enqueue/cancel/queueLength methods.",
|
"description": "Per-session FIFO queue in gateway — serializes concurrent requests instead of rejecting. LaneQueue class with enqueue/cancel/queueLength methods.",
|
||||||
"files_created": [
|
"files_created": [
|
||||||
"src/gateway/lane-queue.ts",
|
"src/gateway/lane-queue.ts",
|
||||||
"src/gateway/lane-queue.test.ts"
|
"src/gateway/lane-queue.test.ts"
|
||||||
@@ -4196,7 +4196,7 @@
|
|||||||
"credential_redaction": {
|
"credential_redaction": {
|
||||||
"priority": "Tier3",
|
"priority": "Tier3",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Expanded redactConfig() from 2 secret locations to 18+ secret fields \u2014 telegram, discord, slack tokens; server.token; all model tier api_key/auth_token; web_search, audio, memory embedding api_keys; webhook secrets; gmail credentials; MCP server env vars.",
|
"description": "Expanded redactConfig() from 2 secret locations to 18+ secret fields — telegram, discord, slack tokens; server.token; all model tier api_key/auth_token; web_search, audio, memory embedding api_keys; webhook secrets; gmail credentials; MCP server env vars.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/gateway/handlers/config.ts",
|
"src/gateway/handlers/config.ts",
|
||||||
"src/gateway/handlers/handlers.test.ts"
|
"src/gateway/handlers/handlers.test.ts"
|
||||||
@@ -4221,7 +4221,7 @@
|
|||||||
"xai_grok_provider": {
|
"xai_grok_provider": {
|
||||||
"priority": "Tier3",
|
"priority": "Tier3",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "xAI as OpenAI-compatible model provider \u2014 reuses OpenAIClient with baseURL https://api.x.ai/v1, XAI_API_KEY env var fallback, pricing for grok-3/grok-3-mini/grok-2/grok-2-mini/grok-3-fast.",
|
"description": "xAI as OpenAI-compatible model provider — reuses OpenAIClient with baseURL https://api.x.ai/v1, XAI_API_KEY env var fallback, pricing for grok-3/grok-3-mini/grok-2/grok-2-mini/grok-3-fast.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/config/schema.ts",
|
"src/config/schema.ts",
|
||||||
"src/daemon/index.ts",
|
"src/daemon/index.ts",
|
||||||
@@ -4231,7 +4231,7 @@
|
|||||||
"voyage_ai_embeddings": {
|
"voyage_ai_embeddings": {
|
||||||
"priority": "Tier3",
|
"priority": "Tier3",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Voyage AI embedding provider for memory/vector search \u2014 OpenAI SDK with baseURL https://api.voyageai.com/v1, defaults to 1024 dimensions, VOYAGE_API_KEY env var.",
|
"description": "Voyage AI embedding provider for memory/vector search — OpenAI SDK with baseURL https://api.voyageai.com/v1, defaults to 1024 dimensions, VOYAGE_API_KEY env var.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/config/schema.ts",
|
"src/config/schema.ts",
|
||||||
"src/memory/embeddings.ts",
|
"src/memory/embeddings.ts",
|
||||||
@@ -4249,7 +4249,7 @@
|
|||||||
"gateway_lock": {
|
"gateway_lock": {
|
||||||
"priority": "Tier4",
|
"priority": "Tier4",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Single-client gateway mode \u2014 if lock=true and a client is connected, reject new connections with code 4003. UI detects locked state.",
|
"description": "Single-client gateway mode — if lock=true and a client is connected, reject new connections with code 4003. UI detects locked state.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/config/schema.ts",
|
"src/config/schema.ts",
|
||||||
"src/gateway/server.ts",
|
"src/gateway/server.ts",
|
||||||
@@ -6922,59 +6922,79 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/backends/native/subagents.test.ts src/tools/builtin/subagents.test.ts src/commands/builtin/index.test.ts src/audit/logger.test.ts src/config/schema.test.ts src/daemon/routing.test.ts passing + pnpm typecheck"
|
"test_status": "pnpm test:run src/backends/native/subagents.test.ts src/tools/builtin/subagents.test.ts src/commands/builtin/index.test.ts src/audit/logger.test.ts src/config/schema.test.ts src/daemon/routing.test.ts passing + pnpm typecheck"
|
||||||
|
},
|
||||||
|
"personal-assistant-productization-phase1-companion-bootstrap-manifest-export": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-27",
|
||||||
|
"updated": "2026-02-27",
|
||||||
|
"summary": "Added a typed companion bootstrap manifest contract (`createCompanionBootstrapManifest`) and companion CLI export mode (`--export-bootstrap <path|->`) so desktop/mobile packaging flows can consume resolved gateway/node/runtime settings without opening a live node connection.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/companion/bootstrapManifest.ts",
|
||||||
|
"src/companion/bootstrapManifest.test.ts",
|
||||||
|
"src/companion/index.ts",
|
||||||
|
"src/cli/companion.ts",
|
||||||
|
"src/cli/companion.test.ts",
|
||||||
|
"README.md",
|
||||||
|
"docs/api/PROTOCOL.md",
|
||||||
|
"docs/architecture/AGENT_DIAGRAM.md",
|
||||||
|
"docs/architecture/GATEWAY_SESSIONS_AND_QUEUE.md",
|
||||||
|
"docs/plans/2026-02-26-personal-assistant-productization-plan.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/cli/companion.test.ts src/companion/bootstrapManifest.test.ts + pnpm typecheck passing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 2568,
|
"total_test_count": 2572,
|
||||||
"all_tests_passing": true,
|
"all_tests_passing": true,
|
||||||
"p0_completion": "3/3 (100%)",
|
"p0_completion": "3/3 (100%)",
|
||||||
"p1_completion": "4/4 (100%)",
|
"p1_completion": "4/4 (100%)",
|
||||||
"p2_completion": "7/7 (100%)",
|
"p2_completion": "7/7 (100%)",
|
||||||
"p3_completion": "completed (group chat, gateway auth, Gemini, OpenRouter, Bedrock, browser control)",
|
"p3_completion": "completed (group chat, gateway auth, Gemini, OpenRouter, Bedrock, browser control)",
|
||||||
"p4_completion": "1/1 (100%) \u2014 multimodal media pipeline",
|
"p4_completion": "1/1 (100%) — multimodal media pipeline",
|
||||||
"p5_completion": "1/1 (100%) \u2014 GitHub Copilot provider with auto-login",
|
"p5_completion": "1/1 (100%) — GitHub Copilot provider with auto-login",
|
||||||
"p6_completion": "4/4 (100%) \u2014 enhanced media pipeline (image.analyze, outbound attachments, gateway attachments, audio transcription)",
|
"p6_completion": "4/4 (100%) — enhanced media pipeline (image.analyze, outbound attachments, gateway attachments, audio transcription)",
|
||||||
"p7_completion": "6/6 (100%) \u2014 web UI dashboard SPA (dashboard, chat, sessions, settings)",
|
"p7_completion": "6/6 (100%) — web UI dashboard SPA (dashboard, chat, sessions, settings)",
|
||||||
"p8_completion": "8/8 (100%) \u2014 agent tools (sessions.list/history/create/delete, agents.list, message.send, cron.list/trigger) + gap analysis audit",
|
"p8_completion": "8/8 (100%) — agent tools (sessions.list/history/create/delete, agents.list, message.send, cron.list/trigger) + gap analysis audit",
|
||||||
"tier1_completion": "5/5 (100%) \u2014 !!think prefix, /verbose command, typing indicators (Discord/WhatsApp), session pruning (TTL), tool groups",
|
"tier1_completion": "5/5 (100%) — !!think prefix, /verbose command, typing indicators (Discord/WhatsApp), session pruning (TTL), tool groups",
|
||||||
"tier2_completion": "4/4 (100%) \u2014 inbound webhooks, vector memory search, Dockerfile, heartbeat monitor",
|
"tier2_completion": "4/4 (100%) — inbound webhooks, vector memory search, Dockerfile, heartbeat monitor",
|
||||||
"tier3_completion": "5/5 (100%) \u2014 lane queue, credential redaction, web UI token dashboard, xAI (Grok) provider, Voyage AI embeddings",
|
"tier3_completion": "5/5 (100%) — lane queue, credential redaction, web UI token dashboard, xAI (Grok) provider, Voyage AI embeddings",
|
||||||
"tier4_completion": "4/4 (100%) \u2014 gateway lock, shell completion, Tailscale Serve/Funnel, DM pairing codes",
|
"tier4_completion": "4/4 (100%) — gateway lock, shell completion, Tailscale Serve/Funnel, DM pairing codes",
|
||||||
"feature_gap_scorecard": "rebaselined 2026-02-26 and updated 2026-02-27 (phase 3 + phase 1 + phase 2 + phase 4 slices) — channel breadth, setup wizard, baseline browser automation, subagent controls, browser workflow reliability primitives (wait/assert/extract/retries/checkpoints/guardrails/budgets), companion reconnect/runtime-handoff foundations, voice reliability hardening (talk controls + TTS fallback/health + interruption-safe cancel semantics), and onboarding first-success funnel improvements are implemented; remaining high-impact personal-assistant gaps center on shipped desktop/mobile companion app surfaces and packaging.",
|
"feature_gap_scorecard": "rebaselined 2026-02-26 and updated 2026-02-27 (phase 3 + phase 1 + phase 2 + phase 4 slices + companion bootstrap packaging contract) — channel breadth, setup wizard, baseline browser automation, subagent controls, browser workflow reliability primitives (wait/assert/extract/retries/checkpoints/guardrails/budgets), companion reconnect/runtime-handoff foundations, voice reliability hardening (talk controls + TTS fallback/health + interruption-safe cancel semantics), onboarding first-success funnel improvements, and companion bootstrap export for app packaging are implemented; remaining high-impact personal-assistant gaps center on shipped desktop/mobile companion app binaries and release packaging.",
|
||||||
"operator_dx_milestone": "Phase 3 (Live Ops Dashboard): 2/2 plans complete \u2014 milestone done",
|
"operator_dx_milestone": "Phase 3 (Live Ops Dashboard): 2/2 plans complete — milestone done",
|
||||||
"dashboard_observability": "completed \u2014 service health graphs + core service log viewer added to web UI via observability RPCs and bounded backend sampling",
|
"dashboard_observability": "completed — service health graphs + core service log viewer added to web UI via observability RPCs and bounded backend sampling",
|
||||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||||
"gmail_filter_creation": "completed \u2014 gmail.filter.create tool added with criteria/action validation; gmail-auth requests explicit gmail.settings.basic + gmail.readonly scopes for filter creation and inbox reads",
|
"gmail_filter_creation": "completed — gmail.filter.create tool added with criteria/action validation; gmail-auth requests explicit gmail.settings.basic + gmail.readonly scopes for filter creation and inbox reads",
|
||||||
"toolloop_action_intent_recovery": "completed \u2014 when a model claims it will execute a tool but emits no tool call, NativeAgent now issues one internal nudge and continues the same turn to execute tools or produce a concrete blocker",
|
"toolloop_action_intent_recovery": "completed — when a model claims it will execute a tool but emits no tool call, NativeAgent now issues one internal nudge and continues the same turn to execute tools or produce a concrete blocker",
|
||||||
"toolloop_execution_claim_recovery": "completed \u2014 when a model claims a known tool already succeeded/failed without emitting a tool call, NativeAgent now nudges once and retries the same turn before returning text",
|
"toolloop_execution_claim_recovery": "completed — when a model claims a known tool already succeeded/failed without emitting a tool call, NativeAgent now nudges once and retries the same turn before returning text",
|
||||||
"daily_briefing_google_scope_remediation": "completed \u2014 calendar.* and tasks.* now append explicit re-auth guidance (`flynn gcal-auth` / `flynn gtasks-auth`) for insufficient-scope errors, and operator runbook includes remediation steps",
|
"daily_briefing_google_scope_remediation": "completed — calendar.* and tasks.* now append explicit re-auth guidance (`flynn gcal-auth` / `flynn gtasks-auth`) for insufficient-scope errors, and operator runbook includes remediation steps",
|
||||||
"council_tool_timeout_override": "completed \u2014 ToolExecutor supports per-tool timeout overrides and council.run now uses a 180s timeout to avoid false 30s council timeouts in the tool loop",
|
"council_tool_timeout_override": "completed — ToolExecutor supports per-tool timeout overrides and council.run now uses a 180s timeout to avoid false 30s council timeouts in the tool loop",
|
||||||
"minimal_tui_multiline_paste_mode": "completed \u2014 minimal TUI now supports `/paste`/`/multiline` multiline compose mode ending with single '.' line, preventing newline truncation for pasted prompts",
|
"minimal_tui_multiline_paste_mode": "completed — minimal TUI now supports `/paste`/`/multiline` multiline compose mode ending with single '.' line, preventing newline truncation for pasted prompts",
|
||||||
"config_profile_consolidation": "completed \u2014 config/paas.yaml is now generated from canonical config/default.yaml + config/profiles/paas.overlay.yaml with CI-checkable drift detection",
|
"config_profile_consolidation": "completed — config/paas.yaml is now generated from canonical config/default.yaml + config/profiles/paas.overlay.yaml with CI-checkable drift detection",
|
||||||
"google_auth_hardening": "completed \u2014 shared Google OAuth runtime helper + auth store (auth.json), legacy token-file migration, refresh persistence, service-wide doctor checks, and unified `flynn google-auth` command",
|
"google_auth_hardening": "completed — shared Google OAuth runtime helper + auth store (auth.json), legacy token-file migration, refresh persistence, service-wide doctor checks, and unified `flynn google-auth` command",
|
||||||
"model_router_correctness": "completed \u2014 fallback paths now avoid duplicate clients, apply retry policy consistently, and reject unsupported OpenAI OAuth tool requests early",
|
"model_router_correctness": "completed — fallback paths now avoid duplicate clients, apply retry policy consistently, and reject unsupported OpenAI OAuth tool requests early",
|
||||||
"native_audio_support": "completed \u2014 smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback, plus 2026-02-23 arg hydration hardening, tool.args_rewritten audit metric, transient fetch retry/timeout hardening, localhost->127.0.0.1 fallback for transcription endpoint connectivity, and whisper docker-compose entrypoint arg fix for port 18801",
|
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback, plus 2026-02-23 arg hydration hardening, tool.args_rewritten audit metric, transient fetch retry/timeout hardening, localhost->127.0.0.1 fallback for transcription endpoint connectivity, and whisper docker-compose entrypoint arg fix for port 18801",
|
||||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) \u2014 context levels, command registry, memory structure. Phase 2: 3/3 (100%) \u2014 component registry, confidence routing, history index. Phase 3: 2/2 (100%) \u2014 adaptive memory/compaction, truthfulness/autonomy hardening",
|
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||||
"deeper_surfaces_behavior_stack_plan": "completed \u2014 documented a decision-complete balanced-hybrid roadmap for OpenClaw-like end-user surface depth plus integrated behavior semantics with phased scope, acceptance gates, and rollout constraints",
|
"deeper_surfaces_behavior_stack_plan": "completed — documented a decision-complete balanced-hybrid roadmap for OpenClaw-like end-user surface depth plus integrated behavior semantics with phased scope, acceptance gates, and rollout constraints",
|
||||||
"deeper_surfaces_phase0_ticket_pack": "completed \u2014 produced an atomic implementation checklist for Phase 0 baseline observability work (audit events, router/gateway emitters, metrics counters, baseline summary tooling, docs sync)",
|
"deeper_surfaces_phase0_ticket_pack": "completed — produced an atomic implementation checklist for Phase 0 baseline observability work (audit events, router/gateway emitters, metrics counters, baseline summary tooling, docs sync)",
|
||||||
"deeper_surfaces_phase0_ticket_01": "completed \u2014 audit schema/logger now capture run lifecycle and reaction decision baseline events (`run.state`, `run.cancel`, `reaction.match`, `reaction.skip`) with regression test coverage",
|
"deeper_surfaces_phase0_ticket_01": "completed — audit schema/logger now capture run lifecycle and reaction decision baseline events (`run.state`, `run.cancel`, `reaction.match`, `reaction.skip`) with regression test coverage",
|
||||||
"deeper_surfaces_phase0_ticket_02": "completed \u2014 gateway + daemon routing emit run lifecycle/cancel telemetry and reaction match/skip audit events with filter summaries and cancellation latency, plus focused tests",
|
"deeper_surfaces_phase0_ticket_02": "completed — gateway + daemon routing emit run lifecycle/cancel telemetry and reaction match/skip audit events with filter summaries and cancellation latency, plus focused tests",
|
||||||
"deeper_surfaces_phase0_ticket_03": "completed \u2014 gateway metrics now track run-state outcomes, cancel latency samples, and reaction decision counters with routing/gateway emitters",
|
"deeper_surfaces_phase0_ticket_03": "completed — gateway metrics now track run-state outcomes, cancel latency samples, and reaction decision counters with routing/gateway emitters",
|
||||||
"deeper_surfaces_phase0_ticket_04": "completed \u2014 added phase-0 baseline summary tooling for run outcomes, cancel latency, and reaction decisions with markdown/json CLI output",
|
"deeper_surfaces_phase0_ticket_04": "completed — added phase-0 baseline summary tooling for run outcomes, cancel latency, and reaction decisions with markdown/json CLI output",
|
||||||
"deeper_surfaces_phase0_ticket_05": "completed \u2014 documented phase-0 telemetry fields/workflow, refreshed architecture/protocol docs, and generated baseline artifacts from a probe log with representative channel + gateway events",
|
"deeper_surfaces_phase0_ticket_05": "completed — documented phase-0 telemetry fields/workflow, refreshed architecture/protocol docs, and generated baseline artifacts from a probe log with representative channel + gateway events",
|
||||||
"next_up": "Replace probe baseline artifacts with live audit samples once gateway/channel sessions emit real run/reaction events",
|
"next_up": "Replace probe baseline artifacts with live audit samples once gateway/channel sessions emit real run/reaction events",
|
||||||
"pi_embedded_canary_spike": "completed \u2014 added optional pi_embedded backend adapter, canary-safe no-tools routing guard, backend success/fallback latency audit events, and docs/diagram updates while native remains default",
|
"pi_embedded_canary_spike": "completed — added optional pi_embedded backend adapter, canary-safe no-tools routing guard, backend success/fallback latency audit events, and docs/diagram updates while native remains default",
|
||||||
"pi_embedded_evaluation_phase": "completed \u2014 final decision rollback (applied in runtime config): Window A failed latency/fallback gates (p50 +259ms, p95 +5695ms, fallback 25%, categories: pi_module_interface/empty_assistant_text); Window B remained sample-insufficient; controlled probes verified guard coverage (pi_no_tools_mode/capability_query/attachments_present each hit once)",
|
"pi_embedded_evaluation_phase": "completed — final decision rollback (applied in runtime config): Window A failed latency/fallback gates (p50 +259ms, p95 +5695ms, fallback 25%, categories: pi_module_interface/empty_assistant_text); Window B remained sample-insufficient; controlled probes verified guard coverage (pi_no_tools_mode/capability_query/attachments_present each hit once)",
|
||||||
"pi_embedded_manual_mode": "completed \u2014 added persisted runtime backend controls for manual Pi activation/deactivation (`/runtime` preferred, `/backend` alias; `status`, `activate pi`, `deactivate pi`, `use config`) while keeping config-driven default routing",
|
"pi_embedded_manual_mode": "completed — added persisted runtime backend controls for manual Pi activation/deactivation (`/runtime` preferred, `/backend` alias; `status`, `activate pi`, `deactivate pi`, `use config`) while keeping config-driven default routing",
|
||||||
"openclaw_gateway_first_tui_runtime_unification": "completed \u2014 shared `/runtime` backend-mode command service across channel router + gateway, plus TUI `/runtime` forwarding through a gateway bridge with daemon/gateway auto-start attach",
|
"openclaw_gateway_first_tui_runtime_unification": "completed — shared `/runtime` backend-mode command service across channel router + gateway, plus TUI `/runtime` forwarding through a gateway bridge with daemon/gateway auto-start attach",
|
||||||
"gateway_startup_eaddrinuse_hardening": "completed \u2014 gateway bind collisions now fail deterministically with explicit error handling and TUI auto-start treats EADDRINUSE as attach race with connect retry",
|
"gateway_startup_eaddrinuse_hardening": "completed — gateway bind collisions now fail deterministically with explicit error handling and TUI auto-start treats EADDRINUSE as attach race with connect retry",
|
||||||
"deeper_surfaces_phase1_run_control": "completed \u2014 interrupt queue mode now enforces latest-wins semantics with channel-path preemption, and gateway emits run_state lifecycle events rendered in the web UI",
|
"deeper_surfaces_phase1_run_control": "completed — interrupt queue mode now enforces latest-wins semantics with channel-path preemption, and gateway emits run_state lifecycle events rendered in the web UI",
|
||||||
"deeper_surfaces_phase2_reactions_v2": "completed \u2014 reaction engine now uses priority/cooldown with non-blocking rules, recursion guard, and routing-level cooldown skip logging",
|
"deeper_surfaces_phase2_reactions_v2": "completed — reaction engine now uses priority/cooldown with non-blocking rules, recursion guard, and routing-level cooldown skip logging",
|
||||||
"deeper_surfaces_phase3_companion_canvas_voice": "completed \u2014 companion reconnect resilience (auto-reconnect with backoff, pending-wait cancellation on disconnect), canvas artifact persistence (SQLite-backed store, daemon-restart durability), voice TTS fallback coverage (text-only reply on TTS failure, no dropped responses)",
|
"deeper_surfaces_phase3_companion_canvas_voice": "completed — companion reconnect resilience (auto-reconnect with backoff, pending-wait cancellation on disconnect), canvas artifact persistence (SQLite-backed store, daemon-restart durability), voice TTS fallback coverage (text-only reply on TTS failure, no dropped responses)",
|
||||||
"deeper_surfaces_phase4_rollout": "completed \u2014 phase 4 rollout and operator readiness plan documented: canary rollout plan by feature flag/surface, explicit rollback playbook, operator docs and architecture/protocol docs synchronized",
|
"deeper_surfaces_phase4_rollout": "completed — phase 4 rollout and operator readiness plan documented: canary rollout plan by feature flag/surface, explicit rollback playbook, operator docs and architecture/protocol docs synchronized",
|
||||||
"post_phase_test_fixes": "completed \u2014 fixed 4 test failures introduced by phases 1-3: iOS/Android push listNodes (missing publishHeartbeat before platform-filtered query), server.test agent.send (run_state events now precede done; added sendAndWaitForDone helper), httpBody 413 (req.destroy() closed socket before response could be sent; replaced with Connection: close header on 413 responses)",
|
"post_phase_test_fixes": "completed — fixed 4 test failures introduced by phases 1-3: iOS/Android push listNodes (missing publishHeartbeat before platform-filtered query), server.test agent.send (run_state events now precede done; added sendAndWaitForDone helper), httpBody 413 (req.destroy() closed socket before response could be sent; replaced with Connection: close header on 413 responses)",
|
||||||
"personal_assistant_productization_plan": "in_progress \u2014 8-10 week phased roadmap active; Phase 3 browser workflow reliability shipped, Phase 1 companion runtime reliability includes reconnect state replay + typed handoff support, Phase 2 voice reliability ships talk controls + TTS provider fallback/health + interruption-safe voice cancel mapping, and Phase 4 onboarding now includes Personal Assistant Mode preset + live readiness checks + first-success guidance. Remaining phase focus: companion app packaging/surfaces.",
|
"personal_assistant_productization_plan": "in_progress — 8-10 week phased roadmap active; Phase 3 browser workflow reliability shipped, Phase 1 companion runtime reliability includes reconnect state replay + typed handoff support, Phase 2 voice reliability ships talk controls + TTS provider fallback/health + interruption-safe voice cancel mapping, Phase 4 onboarding includes Personal Assistant Mode preset + live readiness checks + first-success guidance, and companion bootstrap manifest export now supports desktop/mobile packaging flows. Remaining phase focus: shipped companion app surfaces and release artifacts.",
|
||||||
"subagents_support": "completed \u2014 subagent phases 1-3 shipped with `subagent.spawn/send/list/cancel/delete/summary`, per-child queue mode (`followup|interrupt`), budgets (`max_turns`, `max_total_tokens`, `turn_timeout_ms`), tool-profile overrides, trace-linked audit events, `/subagents` inspection commands, and focused regression tests."
|
"subagents_support": "completed — subagent phases 1-3 shipped with `subagent.spawn/send/list/cancel/delete/summary`, per-child queue mode (`followup|interrupt`), budgets (`max_turns`, `max_total_tokens`, `turn_timeout_ms`), tool-profile overrides, trace-linked audit events, `/subagents` inspection commands, and focused regression tests."
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
@@ -6988,7 +7008,7 @@
|
|||||||
},
|
},
|
||||||
"local-model-message-normalization": {
|
"local-model-message-normalization": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
"summary": "Ollama & llama.cpp tool calling message normalization \u2014 normalizeMessagesForOllama() converts tool_use/tool_result content blocks to Ollama's native role:tool format, normalizeMessagesForLlamaCpp() converts to OpenAI-style tool_calls arrays with hybrid fallback for GGUF templates that drop role:tool messages.",
|
"summary": "Ollama & llama.cpp tool calling message normalization — normalizeMessagesForOllama() converts tool_use/tool_result content blocks to Ollama's native role:tool format, normalizeMessagesForLlamaCpp() converts to OpenAI-style tool_calls arrays with hybrid fallback for GGUF templates that drop role:tool messages.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/models/local/ollama.ts",
|
"src/models/local/ollama.ts",
|
||||||
"src/models/local/ollama.test.ts",
|
"src/models/local/ollama.test.ts",
|
||||||
@@ -7007,7 +7027,7 @@
|
|||||||
"native-audio-support": {
|
"native-audio-support": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
"summary": "Native audio input support \u2014 voice messages passed directly to audio-capable models (Gemini, OpenAI, GitHub) instead of always transcribing via Whisper. Smart routing decides per-model whether to pass raw audio or transcribe first.",
|
"summary": "Native audio input support — voice messages passed directly to audio-capable models (Gemini, OpenAI, GitHub) instead of always transcribing via Whisper. Smart routing decides per-model whether to pass raw audio or transcribe first.",
|
||||||
"phases": {
|
"phases": {
|
||||||
"audio_transcribe_tool": {
|
"audio_transcribe_tool": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
@@ -7044,7 +7064,7 @@
|
|||||||
},
|
},
|
||||||
"tests_and_token_estimation": {
|
"tests_and_token_estimation": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"description": "Audio tests for media helpers, audio token estimation (base64\u2192bytes\u2192duration\u2192tokens at 32 tokens/sec), supports_audio config override wiring",
|
"description": "Audio tests for media helpers, audio token estimation (base64→bytes→duration→tokens at 32 tokens/sec), supports_audio config override wiring",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/models/media.test.ts",
|
"src/models/media.test.ts",
|
||||||
"src/context/tokens.ts",
|
"src/context/tokens.ts",
|
||||||
@@ -7056,7 +7076,7 @@
|
|||||||
},
|
},
|
||||||
"stopreason-normalization": {
|
"stopreason-normalization": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
"summary": "Normalize OpenAI/GitHub finish_reason to Flynn stopReason conventions. OpenAI 'stop' \u2192 'end_turn', 'length' \u2192 'max_tokens', 'tool_calls' with tools \u2192 'tool_use', 'tool_calls' without tools \u2192 'end_turn'. Fixes premature agent loop exit when falling back to GitHub Copilot (Anthropic API quota exhausted). Agent loop now accepts both 'tool_use' and 'tool_calls' as belt-and-suspenders.",
|
"summary": "Normalize OpenAI/GitHub finish_reason to Flynn stopReason conventions. OpenAI 'stop' → 'end_turn', 'length' → 'max_tokens', 'tool_calls' with tools → 'tool_use', 'tool_calls' without tools → 'end_turn'. Fixes premature agent loop exit when falling back to GitHub Copilot (Anthropic API quota exhausted). Agent loop now accepts both 'tool_use' and 'tool_calls' as belt-and-suspenders.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/models/openai.ts",
|
"src/models/openai.ts",
|
||||||
"src/models/openai.test.ts",
|
"src/models/openai.test.ts",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
@@ -6,6 +9,7 @@ const {
|
|||||||
mockGetConfigPath,
|
mockGetConfigPath,
|
||||||
mockRuntimeCtorArgs,
|
mockRuntimeCtorArgs,
|
||||||
mockRuntimeInstances,
|
mockRuntimeInstances,
|
||||||
|
mockCreateCompanionBootstrapManifest,
|
||||||
} = vi.hoisted(() => {
|
} = vi.hoisted(() => {
|
||||||
const runtimeCtorArgs: Array<{ url: string; token?: string; autoReconnect?: boolean }> = [];
|
const runtimeCtorArgs: Array<{ url: string; token?: string; autoReconnect?: boolean }> = [];
|
||||||
const runtimeInstances: Array<{
|
const runtimeInstances: Array<{
|
||||||
@@ -29,12 +33,42 @@ const {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const getConfigPath = vi.fn(() => '/tmp/flynn-config.yaml');
|
const getConfigPath = vi.fn(() => '/tmp/flynn-config.yaml');
|
||||||
|
const createCompanionBootstrapManifest = vi.fn((input: {
|
||||||
|
gatewayUrl: string;
|
||||||
|
gatewayToken?: string;
|
||||||
|
nodeId: string;
|
||||||
|
role: string;
|
||||||
|
platform: string;
|
||||||
|
capabilities: string[];
|
||||||
|
heartbeatSeconds: number;
|
||||||
|
handoffTimeoutMs: number;
|
||||||
|
autoReconnect: boolean;
|
||||||
|
}) => ({
|
||||||
|
schemaVersion: 1,
|
||||||
|
generatedAt: '2026-02-27T00:00:00.000Z',
|
||||||
|
gateway: {
|
||||||
|
url: input.gatewayUrl,
|
||||||
|
...(input.gatewayToken ? { token: input.gatewayToken } : {}),
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
nodeId: input.nodeId,
|
||||||
|
role: input.role,
|
||||||
|
platform: input.platform,
|
||||||
|
capabilities: input.capabilities,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
heartbeatSeconds: input.heartbeatSeconds,
|
||||||
|
handoffTimeoutMs: input.handoffTimeoutMs,
|
||||||
|
autoReconnect: input.autoReconnect,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mockLoadConfigSafe: loadConfigSafe,
|
mockLoadConfigSafe: loadConfigSafe,
|
||||||
mockGetConfigPath: getConfigPath,
|
mockGetConfigPath: getConfigPath,
|
||||||
mockRuntimeCtorArgs: runtimeCtorArgs,
|
mockRuntimeCtorArgs: runtimeCtorArgs,
|
||||||
mockRuntimeInstances: runtimeInstances,
|
mockRuntimeInstances: runtimeInstances,
|
||||||
|
mockCreateCompanionBootstrapManifest: createCompanionBootstrapManifest,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,6 +78,7 @@ vi.mock('./shared.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../companion/index.js', () => ({
|
vi.mock('../companion/index.js', () => ({
|
||||||
|
createCompanionBootstrapManifest: mockCreateCompanionBootstrapManifest,
|
||||||
CompanionRuntimeClient: class {
|
CompanionRuntimeClient: class {
|
||||||
private connectionHandlers: Array<(event: { status: string }) => void> = [];
|
private connectionHandlers: Array<(event: { status: string }) => void> = [];
|
||||||
connect = vi.fn(async () => {
|
connect = vi.fn(async () => {
|
||||||
@@ -80,6 +115,7 @@ describe('companion command', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockRuntimeCtorArgs.length = 0;
|
mockRuntimeCtorArgs.length = 0;
|
||||||
mockRuntimeInstances.length = 0;
|
mockRuntimeInstances.length = 0;
|
||||||
|
mockCreateCompanionBootstrapManifest.mockClear();
|
||||||
mockLoadConfigSafe.mockReturnValue({
|
mockLoadConfigSafe.mockReturnValue({
|
||||||
config: {
|
config: {
|
||||||
server: {
|
server: {
|
||||||
@@ -175,6 +211,94 @@ describe('companion command', () => {
|
|||||||
errSpy.mockRestore();
|
errSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('exports bootstrap manifest and exits without runtime connection', async () => {
|
||||||
|
const tempDir = await mkdtemp(join(tmpdir(), 'flynn-companion-bootstrap-'));
|
||||||
|
const outputPath = join(tempDir, 'manifest.json');
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
|
const program = new Command();
|
||||||
|
const { registerCompanionCommand } = await import('./companion.js');
|
||||||
|
registerCompanionCommand(program);
|
||||||
|
|
||||||
|
await program.parseAsync([
|
||||||
|
'node',
|
||||||
|
'test',
|
||||||
|
'companion',
|
||||||
|
'--platform',
|
||||||
|
'ios',
|
||||||
|
'--node-id',
|
||||||
|
'ios-device',
|
||||||
|
'--heartbeat',
|
||||||
|
'45',
|
||||||
|
'--handoff-timeout',
|
||||||
|
'5000',
|
||||||
|
'--export-bootstrap',
|
||||||
|
outputPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const raw = await readFile(outputPath, 'utf8');
|
||||||
|
const manifest = JSON.parse(raw) as {
|
||||||
|
gateway: { url: string; token?: string };
|
||||||
|
node: { nodeId: string; platform: string; capabilities: string[] };
|
||||||
|
runtime: { heartbeatSeconds: number; handoffTimeoutMs: number };
|
||||||
|
};
|
||||||
|
expect(manifest.gateway).toEqual({ url: 'ws://127.0.0.1:18888', token: 'config-token' });
|
||||||
|
expect(manifest.node.nodeId).toBe('ios-device');
|
||||||
|
expect(manifest.node.platform).toBe('ios');
|
||||||
|
expect(manifest.node.capabilities).toEqual([
|
||||||
|
'ui.canvas',
|
||||||
|
'node.status.write',
|
||||||
|
'node.location.write',
|
||||||
|
'node.push.register',
|
||||||
|
]);
|
||||||
|
expect(manifest.runtime.heartbeatSeconds).toBe(45);
|
||||||
|
expect(manifest.runtime.handoffTimeoutMs).toBe(5000);
|
||||||
|
expect(mockCreateCompanionBootstrapManifest).toHaveBeenCalledOnce();
|
||||||
|
expect(mockRuntimeCtorArgs).toEqual([]);
|
||||||
|
expect(mockRuntimeInstances).toEqual([]);
|
||||||
|
expect(errSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
logSpy.mockRestore();
|
||||||
|
errSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prints bootstrap manifest to stdout when export path is dash', async () => {
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
|
const program = new Command();
|
||||||
|
const { registerCompanionCommand } = await import('./companion.js');
|
||||||
|
registerCompanionCommand(program);
|
||||||
|
|
||||||
|
await program.parseAsync([
|
||||||
|
'node',
|
||||||
|
'test',
|
||||||
|
'companion',
|
||||||
|
'--once',
|
||||||
|
'--export-bootstrap',
|
||||||
|
'-',
|
||||||
|
'--url',
|
||||||
|
'ws://10.0.0.5:19000',
|
||||||
|
'--token',
|
||||||
|
'override-token',
|
||||||
|
'--node-id',
|
||||||
|
'test-node',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const output = logSpy.mock.calls[0]?.[0];
|
||||||
|
expect(typeof output).toBe('string');
|
||||||
|
const manifest = JSON.parse(String(output)) as { gateway: { url: string; token?: string } };
|
||||||
|
expect(manifest.gateway).toEqual({ url: 'ws://10.0.0.5:19000', token: 'override-token' });
|
||||||
|
expect(mockCreateCompanionBootstrapManifest).toHaveBeenCalledOnce();
|
||||||
|
expect(mockRuntimeCtorArgs).toEqual([]);
|
||||||
|
expect(mockRuntimeInstances).toEqual([]);
|
||||||
|
expect(errSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
logSpy.mockRestore();
|
||||||
|
errSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it('sets process exit code when options are invalid', async () => {
|
it('sets process exit code when options are invalid', async () => {
|
||||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|||||||
+31
-1
@@ -1,8 +1,10 @@
|
|||||||
import type { Command } from 'commander';
|
|
||||||
import { hostname } from 'node:os';
|
import { hostname } from 'node:os';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { writeFile } from 'node:fs/promises';
|
||||||
|
import type { Command } from 'commander';
|
||||||
import { CompanionRuntimeClient } from '../companion/index.js';
|
import { CompanionRuntimeClient } from '../companion/index.js';
|
||||||
import type { SetNodeStatusInput } from '../companion/index.js';
|
import type { SetNodeStatusInput } from '../companion/index.js';
|
||||||
|
import { createCompanionBootstrapManifest } from '../companion/index.js';
|
||||||
import { getConfigPath, loadConfigSafe } from './shared.js';
|
import { getConfigPath, loadConfigSafe } from './shared.js';
|
||||||
|
|
||||||
type CompanionPlatform = SetNodeStatusInput['platform'];
|
type CompanionPlatform = SetNodeStatusInput['platform'];
|
||||||
@@ -18,6 +20,7 @@ interface CompanionCommandOptions {
|
|||||||
heartbeat?: string;
|
heartbeat?: string;
|
||||||
handoff?: string;
|
handoff?: string;
|
||||||
handoffTimeout?: string;
|
handoffTimeout?: string;
|
||||||
|
exportBootstrap?: string;
|
||||||
once?: boolean;
|
once?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +108,29 @@ export async function runCompanionSession(options: CompanionCommandOptions): Pro
|
|||||||
const heartbeatSeconds = parseHeartbeatSeconds(options.heartbeat);
|
const heartbeatSeconds = parseHeartbeatSeconds(options.heartbeat);
|
||||||
const handoffMessage = options.handoff?.trim();
|
const handoffMessage = options.handoff?.trim();
|
||||||
const handoffTimeoutMs = parseHandoffTimeoutMs(options.handoffTimeout);
|
const handoffTimeoutMs = parseHandoffTimeoutMs(options.handoffTimeout);
|
||||||
|
const exportBootstrapPath = options.exportBootstrap?.trim();
|
||||||
|
|
||||||
|
if (exportBootstrapPath) {
|
||||||
|
const manifest = createCompanionBootstrapManifest({
|
||||||
|
gatewayUrl,
|
||||||
|
gatewayToken,
|
||||||
|
nodeId,
|
||||||
|
role,
|
||||||
|
platform,
|
||||||
|
capabilities,
|
||||||
|
heartbeatSeconds,
|
||||||
|
handoffTimeoutMs,
|
||||||
|
autoReconnect: !options.once,
|
||||||
|
});
|
||||||
|
const body = `${JSON.stringify(manifest, null, 2)}\n`;
|
||||||
|
if (exportBootstrapPath === '-') {
|
||||||
|
console.log(body.trimEnd());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await writeFile(exportBootstrapPath, body, 'utf8');
|
||||||
|
console.log(`Wrote companion bootstrap manifest to ${exportBootstrapPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const runtime = new CompanionRuntimeClient({
|
const runtime = new CompanionRuntimeClient({
|
||||||
url: gatewayUrl,
|
url: gatewayUrl,
|
||||||
@@ -255,6 +281,10 @@ export function registerCompanionCommand(program: Command): void {
|
|||||||
.option('--heartbeat <seconds>', 'Heartbeat interval in seconds', '30')
|
.option('--heartbeat <seconds>', 'Heartbeat interval in seconds', '30')
|
||||||
.option('--handoff <message>', 'Optional one-shot agent message handoff after registration')
|
.option('--handoff <message>', 'Optional one-shot agent message handoff after registration')
|
||||||
.option('--handoff-timeout <ms>', 'Handoff timeout in milliseconds', '120000')
|
.option('--handoff-timeout <ms>', 'Handoff timeout in milliseconds', '120000')
|
||||||
|
.option(
|
||||||
|
'--export-bootstrap <path>',
|
||||||
|
'Write resolved companion bootstrap manifest JSON (`-` for stdout) and exit',
|
||||||
|
)
|
||||||
.option('--once', 'Connect, register, publish one heartbeat, then exit', false)
|
.option('--once', 'Connect, register, publish one heartbeat, then exit', false)
|
||||||
.action(async (opts: CompanionCommandOptions) => {
|
.action(async (opts: CompanionCommandOptions) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createCompanionBootstrapManifest } from './bootstrapManifest.js';
|
||||||
|
|
||||||
|
describe('createCompanionBootstrapManifest', () => {
|
||||||
|
it('builds a manifest with gateway token and runtime settings', () => {
|
||||||
|
const manifest = createCompanionBootstrapManifest({
|
||||||
|
gatewayUrl: 'ws://127.0.0.1:18800',
|
||||||
|
gatewayToken: 'secret-token',
|
||||||
|
nodeId: 'ios-test-node',
|
||||||
|
role: 'companion',
|
||||||
|
platform: 'ios',
|
||||||
|
capabilities: ['ui.canvas', 'node.push.register'],
|
||||||
|
heartbeatSeconds: 45,
|
||||||
|
handoffTimeoutMs: 5000,
|
||||||
|
autoReconnect: true,
|
||||||
|
generatedAt: new Date('2026-02-27T00:00:00.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(manifest).toEqual({
|
||||||
|
schemaVersion: 1,
|
||||||
|
generatedAt: '2026-02-27T00:00:00.000Z',
|
||||||
|
gateway: {
|
||||||
|
url: 'ws://127.0.0.1:18800',
|
||||||
|
token: 'secret-token',
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
nodeId: 'ios-test-node',
|
||||||
|
role: 'companion',
|
||||||
|
platform: 'ios',
|
||||||
|
capabilities: ['ui.canvas', 'node.push.register'],
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
heartbeatSeconds: 45,
|
||||||
|
handoffTimeoutMs: 5000,
|
||||||
|
autoReconnect: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits an empty gateway token', () => {
|
||||||
|
const manifest = createCompanionBootstrapManifest({
|
||||||
|
gatewayUrl: 'ws://127.0.0.1:18800',
|
||||||
|
gatewayToken: '',
|
||||||
|
nodeId: 'desktop-node',
|
||||||
|
role: 'companion',
|
||||||
|
platform: 'macos',
|
||||||
|
capabilities: ['ui.canvas'],
|
||||||
|
heartbeatSeconds: 30,
|
||||||
|
handoffTimeoutMs: 120000,
|
||||||
|
autoReconnect: false,
|
||||||
|
generatedAt: new Date('2026-02-27T00:00:00.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(manifest.gateway).toEqual({ url: 'ws://127.0.0.1:18800' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { RegisterNodeInput, SetNodeStatusInput } from './runtimeClient.js';
|
||||||
|
|
||||||
|
export type CompanionBootstrapPlatform = SetNodeStatusInput['platform'];
|
||||||
|
|
||||||
|
export interface CompanionBootstrapManifest {
|
||||||
|
schemaVersion: 1;
|
||||||
|
generatedAt: string;
|
||||||
|
gateway: {
|
||||||
|
url: string;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
node: Pick<RegisterNodeInput, 'nodeId' | 'role' | 'capabilities'> & {
|
||||||
|
platform: CompanionBootstrapPlatform;
|
||||||
|
};
|
||||||
|
runtime: {
|
||||||
|
heartbeatSeconds: number;
|
||||||
|
handoffTimeoutMs: number;
|
||||||
|
autoReconnect: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateCompanionBootstrapManifestInput {
|
||||||
|
gatewayUrl: string;
|
||||||
|
gatewayToken?: string;
|
||||||
|
nodeId: string;
|
||||||
|
role: string;
|
||||||
|
platform: CompanionBootstrapPlatform;
|
||||||
|
capabilities: string[];
|
||||||
|
heartbeatSeconds: number;
|
||||||
|
handoffTimeoutMs: number;
|
||||||
|
autoReconnect: boolean;
|
||||||
|
generatedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCompanionBootstrapManifest(
|
||||||
|
input: CreateCompanionBootstrapManifestInput,
|
||||||
|
): CompanionBootstrapManifest {
|
||||||
|
const generatedAt = (input.generatedAt ?? new Date()).toISOString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
generatedAt,
|
||||||
|
gateway: {
|
||||||
|
url: input.gatewayUrl,
|
||||||
|
...(input.gatewayToken && input.gatewayToken.length > 0 ? { token: input.gatewayToken } : {}),
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
nodeId: input.nodeId,
|
||||||
|
role: input.role,
|
||||||
|
platform: input.platform,
|
||||||
|
capabilities: input.capabilities,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
heartbeatSeconds: input.heartbeatSeconds,
|
||||||
|
handoffTimeoutMs: input.handoffTimeoutMs,
|
||||||
|
autoReconnect: input.autoReconnect,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export {
|
|||||||
AndroidCompanionClient,
|
AndroidCompanionClient,
|
||||||
} from './platformClients.js';
|
} from './platformClients.js';
|
||||||
export { CompanionHeartbeatLoop } from './heartbeatLoop.js';
|
export { CompanionHeartbeatLoop } from './heartbeatLoop.js';
|
||||||
|
export { createCompanionBootstrapManifest } from './bootstrapManifest.js';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CompanionRuntimeClientOptions,
|
CompanionRuntimeClientOptions,
|
||||||
@@ -70,3 +71,8 @@ export type {
|
|||||||
CompanionHeartbeatLoopOptions,
|
CompanionHeartbeatLoopOptions,
|
||||||
CompanionHeartbeatLoopState,
|
CompanionHeartbeatLoopState,
|
||||||
} from './heartbeatLoop.js';
|
} from './heartbeatLoop.js';
|
||||||
|
export type {
|
||||||
|
CompanionBootstrapPlatform,
|
||||||
|
CompanionBootstrapManifest,
|
||||||
|
CreateCompanionBootstrapManifestInput,
|
||||||
|
} from './bootstrapManifest.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user