@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/docs/guardian-request-flow.md +35 -0
- package/knip.json +3 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/file-ops-service.test.ts +163 -30
- package/src/__tests__/filesystem-tools.test.ts +23 -24
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/host-file-read-tool.test.ts +16 -19
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/tool-executor.test.ts +5 -1
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/events/host-file.ts +2 -2
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-runtime-assembly.ts +9 -2
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +104 -51
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/approval-card-data.ts +33 -0
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +15 -13
- package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/filesystem/read.ts +27 -10
- package/src/tools/host-filesystem/read.ts +27 -15
- package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
- package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
- package/src/tools/shared/filesystem/types.ts +5 -5
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
package/AGENTS.md
CHANGED
|
@@ -41,6 +41,12 @@ Do not coordinate hook behaviour by re-parsing the tool's JSON response to infer
|
|
|
41
41
|
|
|
42
42
|
Shared mutable resources written by more than one caller (e.g. `dist/` directories produced by `compileApp()`) must be serialised per-resource so concurrent callers cannot race on `rm -rf` + write sequences.
|
|
43
43
|
|
|
44
|
+
## Conversation event delivery and turn presence
|
|
45
|
+
|
|
46
|
+
A `Conversation` has one event sink, fixed at construction and never rebound: top-level conversations are built with the SSE hub (`broadcastMessage`), subagents with the wrapper that re-envelopes their events under the parent. Emit conversation-level events (activity state, confirmation prompts, notifier output, out-of-turn pushes) through `conversation.emit`, which delivers to the sink and then to `addEventObserver` observers. Observers are for policy layered on delivery (the voice bridge auto-resolves approval prompts it has no UI for), never for delivery itself. Do not add a per-subsystem sender slot, a bind/restore step around a turn, or a manual `broadcastMessage` for something the conversation already emits: an emitter that runs outside a live turn (queue drain, ACP or subagent notification, summarize route, call notifiers) reaches every client because the sink is always live.
|
|
47
|
+
|
|
48
|
+
Presence (whether a human is present to see UI and answer prompts) is per-turn state, never derived from delivery. Every dispatch path declares it: `isInteractive` on `runAgentLoop` / `processMessage` / `enqueueMessage`, or a wake's `clientless` pin of `currentTurnIsNonInteractive`. A caller that omits it gets a non-interactive turn (approval-gated tools are denied rather than left waiting on a prompt nobody may answer). `hasNoClient` is a getter over the in-flight turn's presence with no setter, so a new dispatch path cannot inherit whatever the previous turn left behind; if you are reaching for a way to set it, declare interactivity on the turn instead.
|
|
49
|
+
|
|
44
50
|
## Route architecture: shared ROUTES array
|
|
45
51
|
|
|
46
52
|
Routes in `src/runtime/routes/` are being migrated to a **shared `ROUTES` array** that serves as the single source of truth for both the HTTP server and the IPC server. Each route module exports `ROUTES: RouteDefinition[]` (from `routes/types.ts`), and the aggregator `routes/index.ts` collects them.
|
|
@@ -61,7 +67,7 @@ Three response shapes are supported:
|
|
|
61
67
|
- **Binary**: a JSON envelope with `headers: { "content-length": "<n>" }` followed by one binary frame of exactly `n` bytes.
|
|
62
68
|
- **Chunked streaming**: a JSON envelope with `headers: { "transfer-encoding": "chunked" }` followed by one or more binary frames, terminated by a zero-length frame.
|
|
63
69
|
|
|
64
|
-
The server auto-detects legacy newline-delimited JSON from old CLI clients and handles it transparently. New code must use length-prefixed framing via `writeMessage()` / `IpcFrameReader`
|
|
70
|
+
The server auto-detects legacy newline-delimited JSON from old CLI clients and handles it transparently. New code must use length-prefixed framing via `writeMessage()` / `IpcFrameReader` from `@vellumai/ipc-server-utils` (`packages/ipc-server-utils/src/ipc-framing.ts`).
|
|
65
71
|
|
|
66
72
|
### CLI ↔ daemon version skew
|
|
67
73
|
|
|
@@ -69,7 +75,7 @@ The CLI and daemon are always shipped and upgraded together — there is no vers
|
|
|
69
75
|
|
|
70
76
|
### IPC-only routes
|
|
71
77
|
|
|
72
|
-
Some routes are IPC-only (defined in `src/ipc/routes/`, not in the shared array). These are tool/CLI-specific methods (e.g. `wake_conversation`, `upsert_contact`) that have no HTTP counterpart. They follow the existing pattern: define in `src/ipc/routes
|
|
78
|
+
Some routes are IPC-only (defined in `src/ipc/routes/`, not in the shared array). These are tool/CLI-specific methods (e.g. `wake_conversation`, `upsert_contact`) that have no HTTP counterpart. They follow the existing pattern: define a `*_IPC_METHODS` map in `src/ipc/routes/` and add it to the list `AssistantIpcServer` iterates in `src/ipc/assistant-server.ts` (there is no index file; each map is imported by hand).
|
|
73
79
|
|
|
74
80
|
The module-level dependency-injection pattern (`registerFooDeps()`) used by some IPC routes is a known antipattern. New IPC-only routes should avoid it.
|
|
75
81
|
|
package/ARCHITECTURE.md
CHANGED
|
@@ -709,6 +709,8 @@ Live voice STT uses the same `resolveStreamingTranscriber()` path as conversatio
|
|
|
709
709
|
|
|
710
710
|
Live voice TTS uses `streamLiveVoiceTtsAudio()` and the configured `services.tts.provider`. The selected provider must be registered, catalog-compatible, and expose `capabilities.supportsStreaming` plus `synthesizeStream()`. Providers whose catalog entry advertises `supportsStreaming` (currently all four catalog providers: ElevenLabs, Fish Audio, Deepgram, and xAI) satisfy this requirement; a buffered-only provider would remain available for buffered message playback or other supported surfaces, but live voice reports a TTS error instead of silently falling back to buffered playback.
|
|
711
711
|
|
|
712
|
+
The `voiceFrontDoor` prompt skips current-turn legacy and v3 memory retrieval, while prior frozen memory cards and static memory context remain available. The front-door rule escalates rather than guessing when required personal context is absent. The escalated leg runs the ordinary memory pipeline against the latest visible caller prompt before the quality model, with low selector effort. This keeps memory work off the front-door TTFT path without cross-leg speculative state.
|
|
713
|
+
|
|
712
714
|
V1 is local/gateway-scoped. Managed/cloud WebSocket proxy support, cross-region routing, and p50/p95 latency guarantees are out of scope for this version. Metrics frames expose timing data for measurement, but the architecture does not promise a hard latency SLO.
|
|
713
715
|
|
|
714
716
|
**Client service-first boundary:**
|
|
@@ -172,6 +172,21 @@ Ingested pages carry provenance frontmatter with distinct consumers:
|
|
|
172
172
|
machinery (graph extraction, summarization, PKB indexing/filing, PKB
|
|
173
173
|
injection) is suppressed while the substrate is active.
|
|
174
174
|
|
|
175
|
+
#### Live voice front-door memory
|
|
176
|
+
|
|
177
|
+
The live voice front door does not await current-turn memory retrieval. Its
|
|
178
|
+
prompt hook skips legacy graph retrieval, and both v3 injectors skip
|
|
179
|
+
orchestration for `voiceFrontDoor`. Frozen cards from prior turns and the static
|
|
180
|
+
substrate context remain available. When the answer depends on a saved personal
|
|
181
|
+
fact that is absent from that context, the front-door rule escalates instead of
|
|
182
|
+
guessing.
|
|
183
|
+
|
|
184
|
+
The escalated leg runs the ordinary memory pipeline before the quality model.
|
|
185
|
+
V3 retrieval routes on the latest visible caller message, ignoring the hidden
|
|
186
|
+
continuation message used to start that leg. The selector uses low effort to
|
|
187
|
+
keep retrieval latency bounded. This keeps current-turn memory work off the
|
|
188
|
+
front-door TTFT path without maintaining speculative cross-leg state.
|
|
189
|
+
|
|
175
190
|
### Boot-time maintenance
|
|
176
191
|
|
|
177
192
|
`substrate/boot-maintenance.ts`, invoked from the memory plugin's
|
|
@@ -1,40 +1,42 @@
|
|
|
1
|
-
# Browser Use Architecture
|
|
1
|
+
# Browser Use Architecture
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
macOS-originated turns choose a browser backend from a three-tier chain, so the assistant prefers the user's real Chrome session over a sandboxed Playwright instance. The top tier, the host browser proxy, has two transports, both riding the SSE event hub:
|
|
6
6
|
|
|
7
|
-
1. **Chrome Extension
|
|
8
|
-
2. **macOS
|
|
7
|
+
1. **Chrome Extension**: when the Vellum Chrome Extension is installed and paired, `HostBrowserProxy` publishes `host_browser_request` frames to `assistantEventHub` targeted at the extension's SSE subscription; the extension executes CDP commands through `chrome.debugger` and POSTs results to `/v1/host-browser-result`.
|
|
8
|
+
2. **macOS desktop bridge**: when the macOS desktop client is connected but no extension is, the same publish targets the desktop client's SSE subscription; it executes CDP commands against the local Chrome and POSTs results the same way.
|
|
9
9
|
|
|
10
|
-
When neither
|
|
10
|
+
When neither is available, the chain falls through to cdp-inspect (direct Chrome DevTools Protocol attach) before resorting to the local Playwright browser.
|
|
11
11
|
|
|
12
12
|
This document describes the runtime architecture, backend precedence rules, transport matrix, and the manual QA playbook for verifying correct backend selection.
|
|
13
13
|
|
|
14
14
|
## Component Inventory
|
|
15
15
|
|
|
16
|
-
| Component
|
|
17
|
-
|
|
|
18
|
-
| **
|
|
19
|
-
| **
|
|
20
|
-
| **
|
|
21
|
-
| **
|
|
22
|
-
| **
|
|
23
|
-
| **
|
|
24
|
-
| **
|
|
25
|
-
| **
|
|
26
|
-
| **Desktop-auto config**
|
|
16
|
+
| Component | Location | Role |
|
|
17
|
+
| ------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| **HostBrowserProxy** | `daemon/host-browser-proxy.ts` | Lazily-created singleton. Publishes `host_browser_request` frames to the hub with `targetCapability: "host_browser"` and an explicit target client chosen at send time (`resolveTargetClient`), then awaits the matching `host_browser_result`. `Vellum.*` pseudo-methods go only to a chrome-extension client; raw CDP prefers an extension client over the macOS bridge; `targetClientId` pins one; `sourceActorPrincipalId` restricts to that actor's clients. |
|
|
19
|
+
| **events-routes.ts** | `runtime/routes/events-routes.ts` | `GET /v1/events`: registers each SSE client on the hub with the capabilities its `X-Vellum-Interface-Id` supports (`host_browser` for `chrome-extension`; every host-proxy capability for `macos`). |
|
|
20
|
+
| **pair.ts** | `gateway/src/http/routes/pair.ts` | `POST /v1/pair`: loopback-only, rate-limited pairing that mints the extension's `actor_client_v1` JWT for self-hosted deployments. Cloud deployments issue the guardian-bound JWT through the gateway's WorkOS-backed flow. |
|
|
21
|
+
| **CDP Factory** | `tools/browser/cdp-client/factory.ts` | Builds the ordered candidate list and returns a `ScopedCdpClient` with per-invocation failover. The macOS bridge is its internal `"host-bridge"` candidate kind. |
|
|
22
|
+
| **BrowserSessionManager** | `browser-session/manager.ts` | Routes CDP commands through the selected backend with session tracking. |
|
|
23
|
+
| **CdpInspectClient** | `tools/browser/cdp-client/cdp-inspect-client.ts` | Connects to a host Chrome instance via its remote-debugging WebSocket endpoint. |
|
|
24
|
+
| **LocalCdpClient** | `tools/browser/cdp-client/local-cdp-client.ts` | Drives Playwright's CDPSession against the sacrificial-profile browser. |
|
|
25
|
+
| **ExtensionCdpClient** | `tools/browser/cdp-client/extension-cdp-client.ts` | Routes CDP commands through the HostBrowserProxy to the user's real Chrome. |
|
|
26
|
+
| **Desktop-auto config** | `config/schemas/host-browser.ts` | `desktopAuto.enabled` (default `true`) and `desktopAuto.cooldownMs` (default 30s) control automatic cdp-inspect on macOS. |
|
|
27
|
+
|
|
28
|
+
Nothing in the turn layer wires the proxy: it reads the hub's live roster on every send, and a conversation's own event sink is fixed for its life (see `assistant/AGENTS.md`, "Conversation event delivery and turn presence"), so a queued or drained turn reaches the extension exactly like a live one.
|
|
27
29
|
|
|
28
30
|
## Transport Matrix
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
How `host_browser_request` frames reach a client, by originating interface and extension connectivity:
|
|
31
33
|
|
|
32
|
-
| Interface | Extension Connected | Transport
|
|
33
|
-
| ------------------ | ------------------- |
|
|
34
|
-
| `chrome-extension` | Yes (always) |
|
|
35
|
-
| `macos` | Yes |
|
|
36
|
-
| `macos` | No | SSE (`assistantEventHub`)
|
|
37
|
-
| Other | Any |
|
|
34
|
+
| Interface | Extension Connected | Transport | Target | Notes |
|
|
35
|
+
| ------------------ | ------------------- | ------------------------- | --------------------------------------- | --------------------------------------------------------------------------- |
|
|
36
|
+
| `chrome-extension` | Yes (always) | SSE (`assistantEventHub`) | The chrome-extension client | The only transport for chrome-extension turns |
|
|
37
|
+
| `macos` | Yes | SSE (`assistantEventHub`) | The chrome-extension client (preferred) | Browser tools route through the user's real Chrome session |
|
|
38
|
+
| `macos` | No | SSE (`assistantEventHub`) | The macOS desktop client (bridge) | Desktop client executes CDP locally against the user's Chrome |
|
|
39
|
+
| Other | Any | SSE (`assistantEventHub`) | Any connected `host_browser` client | Falls through to cdp-inspect or local Playwright when no client is eligible |
|
|
38
40
|
|
|
39
41
|
## Wire Diagram
|
|
40
42
|
|
|
@@ -45,22 +47,6 @@ macOS app (user message)
|
|
|
45
47
|
POST /v1/messages { interface: "macos", ... }
|
|
46
48
|
|
|
|
47
49
|
v
|
|
48
|
-
conversation-routes.ts
|
|
49
|
-
|-- setTurnInterfaceContext({ userMessageInterface: "macos", ... })
|
|
50
|
-
|-- resolveHostBrowserSender()
|
|
51
|
-
| |
|
|
52
|
-
| +-- ChromeExtensionRegistry.get(guardianId)
|
|
53
|
-
| |
|
|
54
|
-
| +-- entry found? --> registrySender (WS to extension)
|
|
55
|
-
| | hostBrowserSenderOverride = registrySender
|
|
56
|
-
| | provision HostBrowserProxy(registrySender)
|
|
57
|
-
| |
|
|
58
|
-
| +-- entry not found? --> SSE hub sender (default)
|
|
59
|
-
| hostBrowserSenderOverride = undefined
|
|
60
|
-
| provision HostBrowserProxy(onEvent)
|
|
61
|
-
| [macOS natively supports host_browser]
|
|
62
|
-
|
|
|
63
|
-
v
|
|
64
50
|
Agent loop invokes browser tool
|
|
65
51
|
|
|
|
66
52
|
v
|
|
@@ -68,8 +54,9 @@ getCdpClient(toolContext)
|
|
|
68
54
|
|-- toolContext.hostBrowserProxy set?
|
|
69
55
|
| AND hostBrowserProxy.isAvailable()?
|
|
70
56
|
| --> candidate: extension (priority 1)
|
|
71
|
-
| [
|
|
72
|
-
|
|
|
57
|
+
| [HostBrowserProxy publishes to the hub with
|
|
58
|
+
| targetCapability "host_browser"; the target client is
|
|
59
|
+
| the extension when one is connected, else the macOS bridge]
|
|
73
60
|
|
|
|
74
61
|
|-- transportInterface === "macos"
|
|
75
62
|
| AND desktopAuto.enabled?
|
|
@@ -81,7 +68,7 @@ getCdpClient(toolContext)
|
|
|
81
68
|
v
|
|
82
69
|
ScopedCdpClient.send(method, params)
|
|
83
70
|
|
|
|
84
|
-
+-- Try candidate 1 (extension / macOS
|
|
71
|
+
+-- Try candidate 1 (extension / macOS bridge)
|
|
85
72
|
| transport_error? --> failover to candidate 2
|
|
86
73
|
| cdp_error? --> propagate immediately (no failover)
|
|
87
74
|
| success? --> sticky for remainder of invocation
|
|
@@ -96,14 +83,18 @@ ScopedCdpClient.send(method, params)
|
|
|
96
83
|
|
|
97
84
|
## Backend Precedence (macOS)
|
|
98
85
|
|
|
99
|
-
| Priority | Backend | When selected
|
|
100
|
-
| -------- | ---------------------------- |
|
|
101
|
-
| 1 | Extension / macOS host proxy | `
|
|
102
|
-
| 2 | cdp-inspect | Config `enabled: true`, OR macOS + `desktopAuto.enabled` (default) + cooldown not active
|
|
103
|
-
| 3 | Local (Playwright) | Always present as final fallback
|
|
86
|
+
| Priority | Backend | When selected | Transport | Failover trigger |
|
|
87
|
+
| -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
88
|
+
| 1 | Extension / macOS host proxy | `extension` candidate when `hasExtensionClient(actor)` finds a chrome-extension client; otherwise `host-bridge` candidate when `isAvailable(actor)` finds a `host_browser` client and the actor's host-bridge cooldown is not active | SSE (hub) | Transport error (client disconnected, no eligible client, publish failed). A `host-bridge` failure records the per-actor cooldown. |
|
|
89
|
+
| 2 | cdp-inspect | Config `enabled: true`, OR macOS + `desktopAuto.enabled` (default) + cooldown not active | Direct CDP WebSocket | Transport error (endpoint unreachable, WS connect failure). Records cooldown on failure. |
|
|
90
|
+
| 3 | Local (Playwright) | Always present as final fallback | In-process CDP | Errors propagate to the tool |
|
|
104
91
|
|
|
105
92
|
After the first successful CDP command on any backend, that backend becomes **sticky** for the remainder of the tool invocation.
|
|
106
93
|
|
|
94
|
+
## Host-bridge Cooldown
|
|
95
|
+
|
|
96
|
+
When the `host-bridge` candidate fails with a transport error, the factory records a per-actor cooldown (`recordHostBridgeCooldown`, keyed by `sourceActorPrincipalId`, `__default__` when unresolved) for the same `desktopAuto.cooldownMs` window. While it is active, `buildCandidateList` skips the bridge (log `CDP factory: host-bridge skipped (cooldown active)`) and the turn goes straight to cdp-inspect/local. Per-actor rather than process-global because on a multi-actor cloud daemon the bridge reaches a different desktop per actor. The `extension` candidate is never cooled down.
|
|
97
|
+
|
|
107
98
|
## Desktop-auto cdp-inspect Cooldown
|
|
108
99
|
|
|
109
100
|
When cdp-inspect fails with a transport error during a desktop-auto attempt:
|
|
@@ -119,8 +110,8 @@ When cdp-inspect fails with a transport error during a desktop-auto attempt:
|
|
|
119
110
|
|
|
120
111
|
**Setup:**
|
|
121
112
|
|
|
122
|
-
1. Pair the browser extension (
|
|
123
|
-
2.
|
|
113
|
+
1. Pair the browser extension (self-hosted: the extension pairs itself through `POST /v1/pair`; cloud: sign in through the platform).
|
|
114
|
+
2. Verify the extension's SSE subscription is registered (runtime log `subscriber registered (client)` with `interfaceId: "chrome-extension"` and `capabilities: ["host_browser"]`).
|
|
124
115
|
|
|
125
116
|
**Test:**
|
|
126
117
|
|
|
@@ -132,7 +123,7 @@ When cdp-inspect fails with a transport error during a desktop-auto attempt:
|
|
|
132
123
|
- `cdp-factory` log: `CDP factory: built candidate list` with `candidates: [{kind: "extension", ...}, {kind: "cdp-inspect", ...}, {kind: "local", ...}]`
|
|
133
124
|
- `cdp-factory` log: `CDP factory: candidate succeeded, backend is now sticky` with `candidateKind: "extension"`
|
|
134
125
|
- No `browserManager` launch log (Playwright not started).
|
|
135
|
-
-
|
|
126
|
+
- The extension's SSE stream receives `host_browser_request` frames; results arrive as `POST /v1/host-browser-result`.
|
|
136
127
|
|
|
137
128
|
### Scenario 1b: macOS Host Browser Proxy (No Extension)
|
|
138
129
|
|
|
@@ -149,13 +140,13 @@ When cdp-inspect fails with a transport error during a desktop-auto attempt:
|
|
|
149
140
|
|
|
150
141
|
**Expected telemetry/log signals:**
|
|
151
142
|
|
|
152
|
-
- `cdp-factory` log: `CDP factory: built candidate list` with `candidates: [{kind: "
|
|
153
|
-
- `cdp-factory` log: `CDP factory: candidate succeeded, backend is now sticky` with `candidateKind: "
|
|
143
|
+
- `cdp-factory` log: `CDP factory: built candidate list` with `candidates: [{kind: "host-bridge", ...}, {kind: "cdp-inspect", ...}, {kind: "local", ...}]`
|
|
144
|
+
- `cdp-factory` log: `CDP factory: candidate succeeded, backend is now sticky` with `candidateKind: "host-bridge"`
|
|
154
145
|
- No `browserManager` launch log (Playwright not started).
|
|
155
|
-
- SSE
|
|
156
|
-
-
|
|
146
|
+
- The macOS client's SSE stream delivers the `host_browser_request` frames.
|
|
147
|
+
- No failover: the bridge is the first candidate and succeeds.
|
|
157
148
|
|
|
158
|
-
**Difference from Scenario 1:**
|
|
149
|
+
**Difference from Scenario 1:** Same transport, different target: with no chrome-extension client on the hub roster, `resolveTargetClient` selects the macOS desktop client, which registers every host-proxy capability including `host_browser`.
|
|
159
150
|
|
|
160
151
|
### Scenario 2: Extension Absent + cdp-inspect Enabled
|
|
161
152
|
|
|
@@ -205,7 +196,7 @@ In all scenarios, the definitive signal is the `cdp-factory` structured log:
|
|
|
205
196
|
|
|
206
197
|
```
|
|
207
198
|
CDP factory: candidate succeeded, backend is now sticky
|
|
208
|
-
candidateKind: "extension" | "cdp-inspect" | "local"
|
|
199
|
+
candidateKind: "extension" | "host-bridge" | "cdp-inspect" | "local"
|
|
209
200
|
conversationId: "<id>"
|
|
210
201
|
method: "<first CDP method called>"
|
|
211
202
|
```
|
|
@@ -215,3 +206,84 @@ Filter runtime logs with:
|
|
|
215
206
|
```bash
|
|
216
207
|
grep -h "cdp-factory" "$VELLUM_WORKSPACE_DIR"/data/logs/assistant-*.log
|
|
217
208
|
```
|
|
209
|
+
|
|
210
|
+
## Steady-state contract
|
|
211
|
+
|
|
212
|
+
After the first successful Connect, the extension operates as a
|
|
213
|
+
background service with no further user interaction required:
|
|
214
|
+
|
|
215
|
+
1. **Install once**: Load the extension.
|
|
216
|
+
2. **Connect once**: Click Connect in the popup. The worker
|
|
217
|
+
auto-bootstraps credentials (local pair token or cloud JWT) as part
|
|
218
|
+
of the single-click flow.
|
|
219
|
+
3. **Forget it**: The extension keeps its SSE subscription up indefinitely.
|
|
220
|
+
A `chrome.alarms` alarm (`vellum-relay-keepalive`, every 30 s) wakes the
|
|
221
|
+
MV3 service worker and reconnects the stream if it is closed; transient
|
|
222
|
+
drops reconnect with exponential backoff (`sse-connection.ts`). The
|
|
223
|
+
`autoConnect` flag persists across browser sessions so reopening Chrome
|
|
224
|
+
automatically reconnects. An authentication failure is not retried
|
|
225
|
+
silently: it surfaces as the `auth_required` health state below.
|
|
226
|
+
|
|
227
|
+
Users should only interact with the extension again when:
|
|
228
|
+
|
|
229
|
+
- They want to **Pause** (intentionally disconnect and disable
|
|
230
|
+
auto-reconnect).
|
|
231
|
+
- The popup shows **Action required** (`auth_required` or `error` health
|
|
232
|
+
state), meaning automatic recovery has been exhausted.
|
|
233
|
+
|
|
234
|
+
A transient extension disconnect does change backend selection. In auto
|
|
235
|
+
mode `buildCandidateList` reads the hub roster at the start of each
|
|
236
|
+
browser operation: with no chrome-extension client connected it skips the
|
|
237
|
+
`extension` candidate and the chain proceeds to `host-bridge` (macOS),
|
|
238
|
+
cdp-inspect (opt-in, or desktop-auto on macOS), then local; and an
|
|
239
|
+
extension that drops mid-command surfaces a `transport_error`, which
|
|
240
|
+
advances the chained client to the next candidate. Only a dispatch pinned
|
|
241
|
+
with `browser_mode: "extension"` waits through the proxy's reconnect grace
|
|
242
|
+
(`EXTENSION_RECONNECT_GRACE_MS`, 3 s) before failing. `cdp-inspect` is an
|
|
243
|
+
advanced backend for users who cannot install the extension or who need
|
|
244
|
+
broad session-level CDP access; see
|
|
245
|
+
[the `cdp-inspect` backend doc](../../docs/browser-use-cdp-inspect-backend.md).
|
|
246
|
+
|
|
247
|
+
## Known UX considerations
|
|
248
|
+
|
|
249
|
+
### `chrome.debugger` infobar
|
|
250
|
+
|
|
251
|
+
When the Chrome extension calls
|
|
252
|
+
`chrome.debugger.attach(target, requiredVersion)`, Chrome displays a
|
|
253
|
+
persistent yellow infobar at the top of the affected tab saying "Vellum
|
|
254
|
+
started debugging this browser." This is an intentional security
|
|
255
|
+
mitigation; it cannot be suppressed via the public MV3 API.
|
|
256
|
+
|
|
257
|
+
Chrome API notes:
|
|
258
|
+
|
|
259
|
+
- `chrome.debugger.attach(target, requiredVersion, callback)`: three-
|
|
260
|
+
argument form, no options parameter. Chrome 120+.
|
|
261
|
+
(https://developer.chrome.com/docs/extensions/reference/api/debugger)
|
|
262
|
+
- There is no `{ silent: true }` option on attach.
|
|
263
|
+
- The `--silent-debugger-extension-api` command-line flag exists for
|
|
264
|
+
Chromium but (a) requires the user to launch Chrome with the flag,
|
|
265
|
+
(b) is not enabled by default in stable channels, and (c) is not
|
|
266
|
+
something we can enforce on end users.
|
|
267
|
+
- Chrome 126+ added `chrome.debugger.attach` acceptance via `targetId`
|
|
268
|
+
/ `tabId` but did not add a silent-mode option.
|
|
269
|
+
- Closing the infobar does not detach the debugger; it is purely
|
|
270
|
+
informational.
|
|
271
|
+
|
|
272
|
+
Decision: accept the infobar; no public API exists to suppress it. End-user messaging in the
|
|
273
|
+
Mac app popup should explain that the banner is expected and normal
|
|
274
|
+
when Vellum is driving the browser.
|
|
275
|
+
|
|
276
|
+
Alternatives considered:
|
|
277
|
+
|
|
278
|
+
- Playwright / `chrome --remote-debugging-port` in a sacrificial profile
|
|
279
|
+
avoids the infobar but requires installing Chromium and is out-of-
|
|
280
|
+
scope.
|
|
281
|
+
- The assistant-local `cdp-inspect` backend attaches to an existing
|
|
282
|
+
Chrome instance via `chrome://inspect` / `--remote-debugging-port`
|
|
283
|
+
and avoids the per-tab debugger infobar entirely. It is implemented
|
|
284
|
+
and opt-in via `hostBrowser.cdpInspect.enabled`; see
|
|
285
|
+
[the `cdp-inspect` backend doc](../../docs/browser-use-cdp-inspect-backend.md)
|
|
286
|
+
for setup, security trade-offs, and troubleshooting. Note that in auto
|
|
287
|
+
mode it is also the next candidate after `host-bridge` when the
|
|
288
|
+
extension is disconnected at the start of an operation (see the
|
|
289
|
+
steady-state contract above).
|
|
@@ -12,7 +12,7 @@ Flux is a spike. `liveVoice.flux.turnEnd.enabled` defaults to `false`, the exist
|
|
|
12
12
|
|
|
13
13
|
It is stamped in `releaseUtterance` (`live-voice-session.ts`), which every committed turn passes through whichever decider released it, from the same speech-stop anchor on both. So the two arms are one population measured over one span. It is absent only on a turn that never committed, and in push-to-talk, which this spike does not run.
|
|
14
14
|
|
|
15
|
-
It is also the only endpoint number that
|
|
15
|
+
It is also the only endpoint number that a Flux socket teardown stays out of: `releaseUtterance` stamps it before any stop. On a turn Flux itself closed there is no teardown to carry; on a caller-side release `roundTripMs`, `llmFirstDeltaMs`, and `totalMs` still carry one, and so does what the caller hears. Read "The socket teardown, and when it is load-bearing" in section 3 before you compare any of those three against a `deepgram` run.
|
|
16
16
|
|
|
17
17
|
### Why the other endpoint fields are not the comparison
|
|
18
18
|
|
|
@@ -105,18 +105,23 @@ The rest of `liveVoice.flux` is optional and defaulted (`config/schemas/live-voi
|
|
|
105
105
|
|
|
106
106
|
Say the same scripted set of utterances in both arms. Include at least a few deliberate mid-sentence thinking pauses, because that is the case the hold path exists for and the case Flux has to not regress.
|
|
107
107
|
|
|
108
|
-
### The
|
|
108
|
+
### The socket teardown, and when it is load-bearing
|
|
109
109
|
|
|
110
110
|
The Flux adapter implements no `finalizeUtterance`. That is forced by Flux's wire protocol, not a statement that Flux owns the turn boundary, and adding the method as a no-op would break transcript correctness.
|
|
111
111
|
|
|
112
112
|
`parseFluxFrame` emits `final` only on `EndOfTurn`, and that is the adapter's sole source of `final`. Flux offers no mid-stream flush, so `CloseStream` is the only message that makes it answer for a turn still in progress, which is exactly what `stop()` sends. A no-op `finalizeUtterance` would report `finalized` without flushing anything, so a turn released on a caller-side boundary would dispatch on an empty transcript while its real text arrived afterwards and was dropped as a late final segment. With `turnEnd.enabled` at its default `false` that is every turn, because the release always comes from the local silence path. With the latch on it is still every fail-open fallback, every max-duration force-end, and every barge-in, all of which release with a Flux turn open.
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
That reasoning holds for a turn the **caller** releases, and only for those. A turn Flux itself closes needs no flush at all: `EndOfTurn` emits the `final` immediately before `turn-end`, so the transcript is already complete when the release runs, and the stream can stay open (JARVIS-1538). The session reflects that split:
|
|
115
115
|
|
|
116
|
-
-
|
|
117
|
-
-
|
|
116
|
+
- **Provider-closed turns seal in place.** `handleProviderTurnEnd` marks the cycle `providerClosedTurn`, and the release moves it straight to `transcriber_closed` without stopping the transcriber. The stream serves the whole session, and `rearmAfterTurn` re-arms onto it synchronously.
|
|
117
|
+
- **Caller-side releases still close it.** The fail-open deadline, a max-duration force-end, and barge-in all release with a Flux turn potentially still open, and `CloseStream` remains the only message that makes Flux answer for one. Those releases retire the shared stream and tear it down; the next arm dials a fresh one.
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
Consequences for the numbers, all now specific to the caller-side path:
|
|
120
|
+
|
|
121
|
+
- **`endpointCommitLatencyMs` never contains a teardown.** `releaseUtterance` stamps the commit latency and `utteranceEndAtMs` before any stop, so the headline comparison is clean on both arms.
|
|
122
|
+
- **`roundTripMs`, `llmFirstDeltaMs`, and `totalMs` contain it only when the caller released the turn,** which with the latch on means the exceptional paths rather than every turn.
|
|
123
|
+
|
|
124
|
+
Before JARVIS-1538 every utterance dialed its own socket, and the audio arriving during that handshake was lost rather than replayed from the VAD pre-roll buffer. The opening words of each turn after the first went missing ("How many days are in February?" transcribed as "many days are in February?"). A persistent stream removes the handshake, and with it the gap.
|
|
120
125
|
|
|
121
126
|
### What this A/B still does not hold constant
|
|
122
127
|
|
|
@@ -54,6 +54,41 @@ anti-pattern was retired in #35642 and again in the ask_question redesign).
|
|
|
54
54
|
| Kind-specific follow-through | resolver registry (`kind` → resolver) | switch statements in the router |
|
|
55
55
|
| Reply understanding | guardian reply router (codes, buttons, reactions, modes) | per-feature inbound intercepts |
|
|
56
56
|
|
|
57
|
+
## Cards are not conversation history
|
|
58
|
+
|
|
59
|
+
`pairDeliveryWithConversation` persists one message row per delivery so the
|
|
60
|
+
card renders and deep-links. For a guardian card that row is addressed to a
|
|
61
|
+
conversation the request is _about_, not one the assistant is speaking in:
|
|
62
|
+
`buildVellumCardAffinity` pins the vellum card to the originating
|
|
63
|
+
conversation, and a channel card lands in whatever conversation the guardian's
|
|
64
|
+
chat binds to. Either way the row is written straight to the DB by the
|
|
65
|
+
notification pipeline, so the live turn's in-memory history never sees it.
|
|
66
|
+
|
|
67
|
+
That row must never be replayed to the model. The conversation it lands in is
|
|
68
|
+
typically parked mid-approval, with its last assistant message carrying the
|
|
69
|
+
`tool_use` still waiting on this very decision. Replayed, the card sits
|
|
70
|
+
between that `tool_use` and its `tool_result`; history repair reads the pair
|
|
71
|
+
as broken, synthesizes a stub result, and downgrades the real one to text. A
|
|
72
|
+
card left as the tail row instead ends the history on an assistant message,
|
|
73
|
+
which extended-thinking models reject outright ("does not support assistant
|
|
74
|
+
message prefill").
|
|
75
|
+
|
|
76
|
+
`isGuardianCardRow` (`notifications/approval-card-data.ts`) is the one
|
|
77
|
+
definition of which rows those are, read off the card's own `ui_surface` id
|
|
78
|
+
using the same prefixes `approvalCardSurfaceId` recomputes for withdrawal. It
|
|
79
|
+
is derived rather than stored so a row written before the rule existed is
|
|
80
|
+
recognized on the same terms as a new one, with no marker to backfill.
|
|
81
|
+
|
|
82
|
+
**Both history assemblers must consult it.** `Conversation.loadFromDb` builds
|
|
83
|
+
`this.messages`, but Slack conversations do not use that list:
|
|
84
|
+
`loadSlackChronologicalContext` re-reads the rows. A rule applied to only one
|
|
85
|
+
exempts the other channel. Each applies it _after_ its own compaction boundary,
|
|
86
|
+
since both boundaries are computed against unfiltered row lists.
|
|
87
|
+
|
|
88
|
+
Surface state is deliberately NOT filtered this way. `restoreSurfaceStateFromHistory`
|
|
89
|
+
takes the pre-filter window, because a card's Approve/Reject buttons must keep
|
|
90
|
+
routing after a restart even though the card is absent from the model's history.
|
|
91
|
+
|
|
57
92
|
Two instruction modes exist per request kind (`notifications/guardian-question-mode.ts`):
|
|
58
93
|
**approval** ("CODE approve" / approve–reject buttons) and **answer**
|
|
59
94
|
("CODE <your answer>" / option buttons). `pending_question` is answer-mode.
|
package/knip.json
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"src/**/__tests__/**/*.ts",
|
|
5
5
|
"scripts/**/*.ts",
|
|
6
6
|
"src/daemon/main.ts!",
|
|
7
|
+
"src/daemon/windows-compiled-entry.ts!",
|
|
8
|
+
"src/windows-compiled-entry.ts!",
|
|
9
|
+
"src/windows-compiled-worker-entry.ts!",
|
|
7
10
|
"src/plugin-api/index.ts!",
|
|
8
11
|
"src/api/index.ts!",
|
|
9
12
|
"src/config/bundled-skills/**/tools/**/*.ts!"
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* (`gateway/src/http/routes/remote-web-pairing-verification.ts`)
|
|
12
12
|
* - `POST /v1/remote-web/pairing-token` poll + exchange device code
|
|
13
13
|
* (`gateway/src/http/routes/remote-web-pairing-token.ts`)
|
|
14
|
+
* - `GET /v1/remote-web/pairing-requests` list pending challenges
|
|
15
|
+
* (loopback-only)
|
|
16
|
+
* - `POST /v1/remote-web/pairing-requests/approve` approve by request id
|
|
17
|
+
* (loopback-only)
|
|
18
|
+
* - `POST /v1/remote-web/pairing-requests/deny` deny (delete) by request id
|
|
19
|
+
* (loopback-only)
|
|
14
20
|
*
|
|
15
21
|
* These shapes mirror those handlers' request/response bodies exactly so the
|
|
16
22
|
* gateway, the `vellum pair` CLI (`cli/src/commands/pair.ts`), and the web SPA
|
|
@@ -68,6 +74,60 @@ export interface RemoteWebPairingVerificationResponse {
|
|
|
68
74
|
expiresAt: string;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
/**
|
|
78
|
+
* One pending challenge as shown on a host approval surface.
|
|
79
|
+
*
|
|
80
|
+
* The requesting device already sees the plaintext `userCode` in its own
|
|
81
|
+
* challenge response ({@link RemoteWebPairingChallengeResponse.userCode});
|
|
82
|
+
* the loopback-gated list route is the only host-side re-exposure. Displaying
|
|
83
|
+
* it there is what lets the approver match the code against the requesting
|
|
84
|
+
* device's screen: the device-flow anti-phishing binding.
|
|
85
|
+
*/
|
|
86
|
+
export interface RemoteWebPairingRequestSummary {
|
|
87
|
+
/** Opaque server-side id used to approve or deny this request. */
|
|
88
|
+
requestId: string;
|
|
89
|
+
/** The human-readable code the requesting device is displaying (e.g. "ABCD-EFGH"). */
|
|
90
|
+
userCode: string;
|
|
91
|
+
/** Public base URL the challenge was minted for. */
|
|
92
|
+
publicBaseUrl: string;
|
|
93
|
+
/** ISO-8601 instant the challenge was minted. */
|
|
94
|
+
requestedAt: string;
|
|
95
|
+
/** ISO-8601 instant the challenge expires. */
|
|
96
|
+
expiresAt: string;
|
|
97
|
+
/**
|
|
98
|
+
* Client IP of the mint request: the loopback/host address when minted
|
|
99
|
+
* locally, or the edge-observed client address when the mint arrived
|
|
100
|
+
* through the nginx tunnel edge (which stamps it via `proxy_set_header`,
|
|
101
|
+
* so a remote client cannot smuggle a value).
|
|
102
|
+
*/
|
|
103
|
+
requesterIp: string;
|
|
104
|
+
/** User-Agent header of the mint request, or null when absent. */
|
|
105
|
+
requesterUserAgent: string | null;
|
|
106
|
+
/**
|
|
107
|
+
* Whether the mint arrived through the public tunnel edge rather than the
|
|
108
|
+
* host itself.
|
|
109
|
+
*/
|
|
110
|
+
viaEdgeProxy: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `GET /v1/remote-web/pairing-requests` success response body (200). */
|
|
114
|
+
export interface RemoteWebPairingRequestListResponse {
|
|
115
|
+
requests: RemoteWebPairingRequestSummary[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Request body for the pairing-request approve and deny routes. The approve
|
|
120
|
+
* route's success body reuses {@link RemoteWebPairingVerificationResponse}.
|
|
121
|
+
*/
|
|
122
|
+
export interface RemoteWebPairingRequestActionRequest {
|
|
123
|
+
requestId: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `POST /v1/remote-web/pairing-requests/deny` success response body (200). */
|
|
127
|
+
export interface RemoteWebPairingRequestDenyResponse {
|
|
128
|
+
status: "denied";
|
|
129
|
+
}
|
|
130
|
+
|
|
71
131
|
/** `POST /v1/remote-web/pairing-token` request body. */
|
|
72
132
|
export interface RemoteWebPairingTokenRequest {
|
|
73
133
|
/** The `deviceCode` from the challenge. */
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* (`gateway/src/http/routes/remote-web-pairing-verification.ts`)
|
|
12
12
|
* - `POST /v1/remote-web/pairing-token` poll + exchange device code
|
|
13
13
|
* (`gateway/src/http/routes/remote-web-pairing-token.ts`)
|
|
14
|
+
* - `GET /v1/remote-web/pairing-requests` list pending challenges
|
|
15
|
+
* (loopback-only)
|
|
16
|
+
* - `POST /v1/remote-web/pairing-requests/approve` approve by request id
|
|
17
|
+
* (loopback-only)
|
|
18
|
+
* - `POST /v1/remote-web/pairing-requests/deny` deny (delete) by request id
|
|
19
|
+
* (loopback-only)
|
|
14
20
|
*
|
|
15
21
|
* These shapes mirror those handlers' request/response bodies exactly so the
|
|
16
22
|
* gateway, the `vellum pair` CLI (`cli/src/commands/pair.ts`), and the web SPA
|
|
@@ -68,6 +74,60 @@ export interface RemoteWebPairingVerificationResponse {
|
|
|
68
74
|
expiresAt: string;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
/**
|
|
78
|
+
* One pending challenge as shown on a host approval surface.
|
|
79
|
+
*
|
|
80
|
+
* The requesting device already sees the plaintext `userCode` in its own
|
|
81
|
+
* challenge response ({@link RemoteWebPairingChallengeResponse.userCode});
|
|
82
|
+
* the loopback-gated list route is the only host-side re-exposure. Displaying
|
|
83
|
+
* it there is what lets the approver match the code against the requesting
|
|
84
|
+
* device's screen: the device-flow anti-phishing binding.
|
|
85
|
+
*/
|
|
86
|
+
export interface RemoteWebPairingRequestSummary {
|
|
87
|
+
/** Opaque server-side id used to approve or deny this request. */
|
|
88
|
+
requestId: string;
|
|
89
|
+
/** The human-readable code the requesting device is displaying (e.g. "ABCD-EFGH"). */
|
|
90
|
+
userCode: string;
|
|
91
|
+
/** Public base URL the challenge was minted for. */
|
|
92
|
+
publicBaseUrl: string;
|
|
93
|
+
/** ISO-8601 instant the challenge was minted. */
|
|
94
|
+
requestedAt: string;
|
|
95
|
+
/** ISO-8601 instant the challenge expires. */
|
|
96
|
+
expiresAt: string;
|
|
97
|
+
/**
|
|
98
|
+
* Client IP of the mint request: the loopback/host address when minted
|
|
99
|
+
* locally, or the edge-observed client address when the mint arrived
|
|
100
|
+
* through the nginx tunnel edge (which stamps it via `proxy_set_header`,
|
|
101
|
+
* so a remote client cannot smuggle a value).
|
|
102
|
+
*/
|
|
103
|
+
requesterIp: string;
|
|
104
|
+
/** User-Agent header of the mint request, or null when absent. */
|
|
105
|
+
requesterUserAgent: string | null;
|
|
106
|
+
/**
|
|
107
|
+
* Whether the mint arrived through the public tunnel edge rather than the
|
|
108
|
+
* host itself.
|
|
109
|
+
*/
|
|
110
|
+
viaEdgeProxy: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `GET /v1/remote-web/pairing-requests` success response body (200). */
|
|
114
|
+
export interface RemoteWebPairingRequestListResponse {
|
|
115
|
+
requests: RemoteWebPairingRequestSummary[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Request body for the pairing-request approve and deny routes. The approve
|
|
120
|
+
* route's success body reuses {@link RemoteWebPairingVerificationResponse}.
|
|
121
|
+
*/
|
|
122
|
+
export interface RemoteWebPairingRequestActionRequest {
|
|
123
|
+
requestId: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `POST /v1/remote-web/pairing-requests/deny` success response body (200). */
|
|
127
|
+
export interface RemoteWebPairingRequestDenyResponse {
|
|
128
|
+
status: "denied";
|
|
129
|
+
}
|
|
130
|
+
|
|
71
131
|
/** `POST /v1/remote-web/pairing-token` request body. */
|
|
72
132
|
export interface RemoteWebPairingTokenRequest {
|
|
73
133
|
/** The `deviceCode` from the challenge. */
|