@oar-sh/oar 0.9.0
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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
package/server/README.md
ADDED
|
@@ -0,0 +1,1379 @@
|
|
|
1
|
+
# Copilot Web Proxy
|
|
2
|
+
|
|
3
|
+
A Node.js web server that lets you chat with your local coding agents from any device via a browser.
|
|
4
|
+
|
|
5
|
+
Three runtimes can serve a conversation, selected when the conversation is created:
|
|
6
|
+
|
|
7
|
+
- **GitHub Copilot CLI** (default) — executed by the `web-relay` CLI extension and its session workers.
|
|
8
|
+
- **OpenAI (BYOK)** — the Copilot CLI running in BYOK mode against an OpenAI-compatible endpoint, plus a direct Images API path for image conversations.
|
|
9
|
+
- **Claude (Agent SDK)** — a dedicated Node worker in `server/claude-worker/` that speaks the same relay contracts.
|
|
10
|
+
|
|
11
|
+
See [Claude Agent SDK provider](#claude-agent-sdk-provider) for the third runtime.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This project now runs in package-wide ESM mode (`"type": "module"` in `package.json`).
|
|
20
|
+
|
|
21
|
+
## Starting the Server
|
|
22
|
+
|
|
23
|
+
If you run `gh copilot` and the web-relay extension is loaded (project-local or user-global),
|
|
24
|
+
the extension auto-starts `server.js` when needed and keeps the relay listener singleton
|
|
25
|
+
while letting session-affine CLI workers run in parallel.
|
|
26
|
+
|
|
27
|
+
If `tty-console` is installed as an optional dependency, the server starts it on an interactive
|
|
28
|
+
terminal and stores its config/history files under `server/logs/`.
|
|
29
|
+
When active, worker loop telemetry is summarized in the title bar (`Clients`, `Pen`, `Proc`,
|
|
30
|
+
`parked`, running `CLIs`) and routine worker dequeue events are not printed as repeating logs.
|
|
31
|
+
|
|
32
|
+
On Linux/macOS, session-affine worker launches prefer detached `tmux` sessions when `tmux`
|
|
33
|
+
is available, using the worker SDK session id as the tmux session name so you can attach
|
|
34
|
+
for debugging without changing the Windows launch path. Worker processes now launch from the
|
|
35
|
+
relay repository root so the project-local `web-relay` extension is always discoverable; the
|
|
36
|
+
session's actual target workspace is still passed via `COPILOT_WORKSPACE_ROOT` / `INIT_CWD`.
|
|
37
|
+
|
|
38
|
+
The web client now includes a read-only **Inspect tmux console** action in the conversation
|
|
39
|
+
three-dot menu. It streams only from active relay session workers and only while the viewer
|
|
40
|
+
is actively watching that session.
|
|
41
|
+
|
|
42
|
+
> **Single-owner rule:** Use either extension-managed relay transport **or** standalone `relay.mjs`, never both at the same time.
|
|
43
|
+
> Agent/runtime restart policy is defined in `.github/copilot-instructions.md`.
|
|
44
|
+
|
|
45
|
+
Launch Copilot CLI from the repository root with extensions enabled and no initial user prompt:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run copilot:relay
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That runs `gh copilot -- --allow-all` from the repo root, so the
|
|
52
|
+
extension is discovered and the relay starts immediately. If you installed the extension
|
|
53
|
+
globally, plain `gh copilot` from any repository is enough.
|
|
54
|
+
|
|
55
|
+
Or manually:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node server/server.js
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This starts the web server and stays attached to the terminal, respawning its
|
|
62
|
+
worker child across restarts. Ctrl+C (or closing the terminal) stops both.
|
|
63
|
+
A Copilot CLI session with the extension attaches separately.
|
|
64
|
+
|
|
65
|
+
If you install this repo locally with `npm link` or `npm install -g .`, the `copilot-remote`
|
|
66
|
+
command starts the web relay server if needed and then launches `gh copilot` in the same shell
|
|
67
|
+
from whatever folder you run it in. If a relay is already live, it reuses it instead of starting
|
|
68
|
+
a second owner.
|
|
69
|
+
|
|
70
|
+
`copilot-remote` also prepares a user-global `web-relay` extension wrapper by default. You can run
|
|
71
|
+
`copilot-remote --install-extension` to install/update only the wrapper and exit.
|
|
72
|
+
|
|
73
|
+
Relay server output is redirected to a logfile under `%LOCALAPPDATA%\copilot-remote\logs` by
|
|
74
|
+
default (or `COPILOT_WEB_RELAY_LOG_DIR` if set), so the terminal stays reserved for the CLI.
|
|
75
|
+
|
|
76
|
+
The global launcher no longer injects a bootstrap prompt; it just starts `gh copilot` directly
|
|
77
|
+
after the relay is ready.
|
|
78
|
+
|
|
79
|
+
If you need a specific `server/config.json` for token or tunnel settings, set
|
|
80
|
+
`COPILOT_WEB_RELAY_CONFIG` to that file before launching. The global npm install does not include
|
|
81
|
+
the repo-local gitignored config file by default.
|
|
82
|
+
|
|
83
|
+
Mode summary — one entry point, role chosen by argv:
|
|
84
|
+
|
|
85
|
+
- `node server/server.js` (= `npm start`): the process stays attached as a supervisor and runs
|
|
86
|
+
`server-runtime.mjs` in a worker child it marks with `--relay-runtime`. Exit 75 relaunches the
|
|
87
|
+
worker; other non-zero exits are retried 3 times before the supervisor gives up.
|
|
88
|
+
- `node server/server.js --supervised`: runs the server in this process and exits 75 instead of
|
|
89
|
+
restarting itself, leaving that to whoever spawned it. This is what the CLI extension passes.
|
|
90
|
+
- `node server/relay.mjs`: the standalone Copilot relay, started by hand after the server is up.
|
|
91
|
+
Manual escape hatch only — never alongside extension-managed transport.
|
|
92
|
+
|
|
93
|
+
Role flags are argv, not env, on purpose: the server's environment is inherited by tmux worker
|
|
94
|
+
sessions and by the Copilot CLI it launches, and the extension spawns the next server from that
|
|
95
|
+
same environment — so an env flag would decide the role of unrelated servers down the tree.
|
|
96
|
+
|
|
97
|
+
On Windows, the visible relay launcher path now targets a stable per-workspace Windows Terminal window name so later foreground launches reuse the same window instead of opening new desktop windows. Keep the hidden/stdio path as a fallback only.
|
|
98
|
+
|
|
99
|
+
### Runtime safety checklist (avoid duplicate relay workers)
|
|
100
|
+
|
|
101
|
+
1. Stop stale detached watchdog/relay processes before restart.
|
|
102
|
+
2. Keep exactly one listener on port `3333`.
|
|
103
|
+
3. The relay singleton lock is stored at `server/data/relay-server.lock` (stale locks are auto-recovered).
|
|
104
|
+
4. In extension-managed mode, do not start a second server or run `node relay.mjs`.
|
|
105
|
+
5. Verify `/api/status` shows `cliOnline: true` and queue counts are moving or zero.
|
|
106
|
+
6. Follow relay restart policy from `.github/copilot-instructions.md`.
|
|
107
|
+
7. In extension-managed mode, use `POST /api/relay/shutdown` for manual restart requests.
|
|
108
|
+
- It is queued until the relay is idle, so it will not stop an in-flight turn immediately.
|
|
109
|
+
- Send `{ "restart": true }` when you want a self-restart instead of a plain shutdown.
|
|
110
|
+
8. Do not run tests that spawn Copilot CLI clients unless the user explicitly permits it.
|
|
111
|
+
|
|
112
|
+
Script necessity note:
|
|
113
|
+
|
|
114
|
+
- Extension-managed relay does not call npm scripts directly; it starts `server.js --supervised` itself.
|
|
115
|
+
- Extension-managed relay supervision includes bounded auto-restart while the CLI session is active,
|
|
116
|
+
and is now the only supervisor in that mode.
|
|
117
|
+
- `npm start` is a plain alias for `node server/server.js`; there is no other start script.
|
|
118
|
+
- Manual restart policy is defined in `.github/copilot-instructions.md`.
|
|
119
|
+
|
|
120
|
+
## Global extension install (user-scoped)
|
|
121
|
+
|
|
122
|
+
Copilot also scans a user extensions directory, so you can make this extension available
|
|
123
|
+
across repositories:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
C:\Users\<you>\.copilot\extensions\web-relay\extension.mjs
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
With global install, starting `gh copilot` in any workspace will load this extension and keep
|
|
130
|
+
the relay tied to that workspace CWD instead of forcing a fixed repo launcher.
|
|
131
|
+
|
|
132
|
+
If you also keep a project-local copy, extension management can show two `web-relay` entries.
|
|
133
|
+
Keep only one active to prevent duplicate loading.
|
|
134
|
+
|
|
135
|
+
For global install, use `copilot-remote --install-extension` (recommended) or manually place a
|
|
136
|
+
wrapper `extension.mjs` there that imports your repository extension entrypoint. Then set one of
|
|
137
|
+
these environment variables so it can find your relay server files:
|
|
138
|
+
|
|
139
|
+
- `COPILOT_WEB_RELAY_SERVER_DIR` (recommended) → absolute path to the `server` folder
|
|
140
|
+
- `COPILOT_WEB_RELAY_ROOT` → repo root that contains `server\`
|
|
141
|
+
- Optional overrides: `COPILOT_WEB_RELAY_CONFIG`, `COPILOT_WEB_RELAY_TOOLS`, `COPILOT_WEB_RELAY_LOG_DIR`
|
|
142
|
+
|
|
143
|
+
Project-local extensions still take precedence if the same extension name exists in both locations.
|
|
144
|
+
|
|
145
|
+
The startup banner shows your access URLs and token.
|
|
146
|
+
|
|
147
|
+
## Accessing the UI
|
|
148
|
+
|
|
149
|
+
Open in any browser:
|
|
150
|
+
```
|
|
151
|
+
http://<your-pc-ip>:3333/
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
If `localhostOnly` is enabled in `config.json`, the server listens only on loopback and you must use:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
http://localhost:3333/
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`localhostOnly` affects only the local relay listener. SSH reverse tunnel exposure is configured separately with `sshTunnel.remoteBind`. The Cloudflare Tunnel mode (`cloudflaredTunnel`) needs no public bind at all: `cloudflared` connects outbound to Cloudflare and proxies back into `127.0.0.1`.
|
|
161
|
+
|
|
162
|
+
Sign in once with the token prompt; the browser stores the session in an HttpOnly cookie.
|
|
163
|
+
Each CLI session now tracks its own workspace root:
|
|
164
|
+
- the running CLI keeps its learned runtime CWD
|
|
165
|
+
- the **🗂️ Change CWD** menu updates that session's persisted next-launch CWD
|
|
166
|
+
- the workspace browser follows the selected session's effective CWD instead of one relay-global root
|
|
167
|
+
|
|
168
|
+
Startup CWD learning prefers explicit session/launcher hints such as `COPILOT_WORKSPACE_ROOT`,
|
|
169
|
+
then other runtime cwd hints (`INIT_CWD`, `PWD`, session metadata). The extension startup sync
|
|
170
|
+
deliberately skips `process.cwd()` so the relay host directory never silently masquerades as a
|
|
171
|
+
project root. Sessions without an explicit configured CWD launch in the relay's working directory.
|
|
172
|
+
|
|
173
|
+
A plain `cd <path>` chat command persists to that conversation's configured CWD only — it does not
|
|
174
|
+
affect other sessions or the global relay root.
|
|
175
|
+
|
|
176
|
+
Use the chat header **⋯** menu and choose **🗂️ Change CWD** to switch the selected session's next
|
|
177
|
+
launch directory from the list of known directories. The CWD picker always stays available; the
|
|
178
|
+
launch action is only enabled for the selected CLI when it is not running.
|
|
179
|
+
|
|
180
|
+
### Socket events for CWD changes
|
|
181
|
+
|
|
182
|
+
| Event | Emitter | Meaning |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `workspace_root_changed` | Admin CWD picker (`POST /api/workspace-root`) | Global relay root changed; affects all sessions that have no per-session root configured |
|
|
185
|
+
| `conversation_workspace_root_updated` | Chat `/cd` command and conversation CWD API | Per-conversation configured root updated; only the referenced conversation is affected |
|
|
186
|
+
|
|
187
|
+
## Install as an app
|
|
188
|
+
|
|
189
|
+
The UI is now a Progressive Web App. On Android Chrome, use the browser menu to choose
|
|
190
|
+
**Install app** or **Add to Home screen**. Installed app mode now prefers fullscreen launch
|
|
191
|
+
where supported (with standalone fallback), and hides install/fullscreen header buttons.
|
|
192
|
+
|
|
193
|
+
When opened in a regular browser tab, the in-app **Install** button remains available
|
|
194
|
+
in the chat header (shown as `⬇` on small screens).
|
|
195
|
+
|
|
196
|
+
You can rename the installed app label from **⚙️ Settings → Install app name**.
|
|
197
|
+
The new label is stored per browser and used for future installs; some platforms may
|
|
198
|
+
require reinstalling the app before the launcher label updates.
|
|
199
|
+
|
|
200
|
+
You can also hide the **💤 Suspend host** action from **⚙️ Settings** with the
|
|
201
|
+
**Show Suspend host action** checkbox. This only controls UI visibility; it does not
|
|
202
|
+
change host suspend implementation behavior.
|
|
203
|
+
|
|
204
|
+
If you host the relay behind a subpath, set `remotePath` in `server/config.json` to that public path prefix and open the URL with a trailing slash so the PWA scope matches correctly for install prompts. The relay serves a path-relative manifest identity (`id`, `start_url`, `scope`) so each install stays bound to its own URL subtree and avoids cross-app collisions on shared origins.
|
|
205
|
+
|
|
206
|
+
## Model Selection
|
|
207
|
+
|
|
208
|
+
The composer includes a model picker next to the Send button.
|
|
209
|
+
Models are now populated dynamically from the active Copilot CLI runtime using
|
|
210
|
+
raw model IDs (no relay-specific aliases). The selected model ID is sent as-is
|
|
211
|
+
for each message.
|
|
212
|
+
|
|
213
|
+
Behavior notes:
|
|
214
|
+
|
|
215
|
+
- The picker refreshes from live CLI model discovery when relay status changes.
|
|
216
|
+
- Selection is persisted in browser storage and reused if still available.
|
|
217
|
+
- If live discovery is temporarily unavailable, the relay can use cached/current
|
|
218
|
+
model state and shows a warning banner.
|
|
219
|
+
- In extension-managed mode, the relay still switches model per message and
|
|
220
|
+
reports the active model used in the response.
|
|
221
|
+
|
|
222
|
+
### OpenAI API key
|
|
223
|
+
|
|
224
|
+
Open **Settings → OpenAI API key** to save an OpenAI key and exact model ID
|
|
225
|
+
(`gpt-4o` by default). Use the provider toggle to switch new conversations
|
|
226
|
+
between OpenAI and GitHub Copilot without re-entering the saved key. While
|
|
227
|
+
enabled, newly created extension-managed conversation workers start with Copilot
|
|
228
|
+
CLI BYOK environment variables pointing to `https://api.openai.com/v1`.
|
|
229
|
+
Zero-message conversations follow provider changes; started conversations keep
|
|
230
|
+
their original provider and lock the composer to their assigned model.
|
|
231
|
+
|
|
232
|
+
GPT-5 and OpenAI reasoning-model sessions use the Responses API automatically;
|
|
233
|
+
other OpenAI models retain the broader-compatible Chat Completions wire format.
|
|
234
|
+
OpenAI BYOK sessions currently use reasoning effort `none`; GitHub Copilot
|
|
235
|
+
reasoning choices remain separate even when both providers expose the same model
|
|
236
|
+
ID.
|
|
237
|
+
|
|
238
|
+
Saving or re-enabling the key refreshes OpenAI's `/v1/models` list and adds
|
|
239
|
+
candidate OpenAI model IDs to the composer picker. The Settings model-variant
|
|
240
|
+
refresh performs the same discovery while OpenAI is enabled and retains the
|
|
241
|
+
cached list if discovery fails. Starting a conversation uses the cached list; it
|
|
242
|
+
does not make another discovery request. The model selected when creating a
|
|
243
|
+
conversation is persisted on that conversation and used as its `COPILOT_MODEL`;
|
|
244
|
+
the Settings model is only the default. Starting a chat from a locked OpenAI
|
|
245
|
+
conversation opens an independent model chooser without changing the active
|
|
246
|
+
conversation.
|
|
247
|
+
|
|
248
|
+
Disabling OpenAI retains the key and cached model list. Removing the key is a
|
|
249
|
+
separate destructive action and makes subsequent conversations use GitHub
|
|
250
|
+
Copilot.
|
|
251
|
+
An existing OpenAI-assigned conversation can continue while its worker remains
|
|
252
|
+
running, but cannot launch a replacement worker until an OpenAI key is configured.
|
|
253
|
+
|
|
254
|
+
### Claude models
|
|
255
|
+
|
|
256
|
+
When the Claude provider is enabled, discovered `claude-*` IDs join the same catalog. The composer
|
|
257
|
+
only offers models the active conversation's provider can serve, so Claude models are hidden in
|
|
258
|
+
Copilot/OpenAI conversations and vice versa.
|
|
259
|
+
|
|
260
|
+
**Select Models** has one tab per runtime — **Copilot**, **OpenAI**, **Claude SDK**, **Cursor SDK** —
|
|
261
|
+
and each tab lists only rows contributed by that runtime. The Claude and Cursor tabs write their
|
|
262
|
+
selection through `POST /api/settings/claude` / `POST /api/settings/cursor` (`enabledModels`) rather
|
|
263
|
+
than the model-variant catalog; the configured default model is always enabled and cannot be
|
|
264
|
+
deselected, since deselecting it would leave that provider's conversations without a model. An empty
|
|
265
|
+
selection means "all available".
|
|
266
|
+
|
|
267
|
+
`POST /api/model-variants/refresh` refreshes all enabled providers concurrently and reports
|
|
268
|
+
`openAIModelDiscovery` and `claudeModelDiscovery` separately, so one provider's failed discovery does
|
|
269
|
+
not hide another's success — the cached list is kept and a warning is surfaced in the UI.
|
|
270
|
+
|
|
271
|
+
## Claude Agent SDK provider
|
|
272
|
+
|
|
273
|
+
Claude conversations are executed by `server/claude-worker/claude-session-worker.mjs`, a plain Node
|
|
274
|
+
process spawned per conversation. It is not part of the Copilot CLI extension: it connects to the
|
|
275
|
+
same worker WebSocket, sends the same heartbeats, and posts to the same activity/stream/thought/
|
|
276
|
+
response endpoints, but runs turns through `@anthropic-ai/claude-agent-sdk` instead of the Copilot SDK.
|
|
277
|
+
|
|
278
|
+
### Enabling
|
|
279
|
+
|
|
280
|
+
**Settings → Claude (Agent SDK) → Enable Claude for New Chat model selection.**
|
|
281
|
+
|
|
282
|
+
Authentication uses the relay host's own logged-in Claude credentials (`~/.claude`) — the relay never
|
|
283
|
+
stores an Anthropic API key. Run `claude` on the host and complete login before enabling. A turn that
|
|
284
|
+
fails to authenticate is answered with a system note telling you to do exactly that, rather than
|
|
285
|
+
being retried silently.
|
|
286
|
+
|
|
287
|
+
Enabling (or changing the default model) triggers model discovery: a short-lived idle `query()` is
|
|
288
|
+
opened purely to serve a `supportedModels()` control request, with a 20 s timeout. Discovered
|
|
289
|
+
`claude-*` IDs — including bracketed capability variants such as `claude-opus-5[1m]` — are stored and
|
|
290
|
+
added to the model catalog. Bare aliases (`default`, `sonnet`) are dropped in favour of the explicit IDs.
|
|
291
|
+
In the composer, `[1m]` variants are folded into their base model: the base ID is the only dropdown
|
|
292
|
+
entry, and the variant is selected through the context-size dropdown (`long_context` composes the
|
|
293
|
+
`[1m]` ID on send and in the persisted per-conversation model preference).
|
|
294
|
+
|
|
295
|
+
Disabling Claude rebinds conversations that have not yet sent a message back to the default provider,
|
|
296
|
+
mirroring the OpenAI key-removal reconciliation. Conversations that have already started keep their
|
|
297
|
+
provider, and other providers' bindings are never touched.
|
|
298
|
+
|
|
299
|
+
### Per-turn behavior
|
|
300
|
+
|
|
301
|
+
| Relay mode | Claude mapping |
|
|
302
|
+
| ----------- | --------------------------------------------------------------------------- |
|
|
303
|
+
| `plan` | SDK `permissionMode: 'plan'`; `ExitPlanMode` publishes a **plan_ready** board, then the tool call is **denied** so the turn ends for review instead of rolling straight into implementation — the board's choice starts the next turn in the chosen mode |
|
|
304
|
+
| `ask` | System prompt appended to prefer `AskUserQuestion` before implementing |
|
|
305
|
+
| `agent` | Default permission mode, unmodified preset prompt |
|
|
306
|
+
| `autopilot` | System prompt appended to keep moving unless input is truly blocking |
|
|
307
|
+
|
|
308
|
+
- **Model and reasoning effort are per turn.** Each turn is a fresh `query()` with `resume`, so both
|
|
309
|
+
can change between messages. Effort levels are `none` (SDK default) plus whatever the model reports
|
|
310
|
+
(`low`, `medium`, `high`, `xhigh`, `max`), plus **Ultracode** — see below.
|
|
311
|
+
- **Ultracode** is offered on every model whose discovered efforts include `xhigh`. It is not an SDK
|
|
312
|
+
effort level but a session-scoped settings flag, so the relay carries `ultracode` as a sentinel on
|
|
313
|
+
the ordinary effort ladder (`withClaudeUltracodeTier` in `services/provider-reasoning-effort.mjs`,
|
|
314
|
+
the single source for all Claude effort lists) and only the worker translates it: a fresh session
|
|
315
|
+
spawns with `effort: 'xhigh'` and `settings: { ultracode: true, enableWorkflows: true }`, and a
|
|
316
|
+
live session is toggled in or out with `applyFlagSettings` (`claudeUltracodeFlagSettings`).
|
|
317
|
+
`enableWorkflows` is passed explicitly because the worker loads no filesystem settings. It is never
|
|
318
|
+
a silent default — the selectors label the rung "Ultracode" and warn about the token cost.
|
|
319
|
+
- **Tools are auto-allowed**, matching the Copilot workers' `--allow-all` posture. Only two tools are
|
|
320
|
+
intercepted: `AskUserQuestion` (bridged to relay question cards) and `ExitPlanMode` (plan board;
|
|
321
|
+
denied after the board posts — the plan text comes from the tool input, or from the CLI's
|
|
322
|
+
`planFilePath` when no inline plan is sent).
|
|
323
|
+
- **Attachments** — images up to 5 MB are inlined as base64 content blocks; larger images and all
|
|
324
|
+
non-image files are passed as absolute path references for Claude's `Read` tool.
|
|
325
|
+
- **Subagents** — `forwardSubagentText` is on, so subagent text, thoughts, and activity stream into
|
|
326
|
+
their own nested bubbles. `abort_subagent` maps to `query.stopTask()` for a backgrounded subagent;
|
|
327
|
+
one running inside the current turn is still answered "not supported". Full-turn **Stop** works.
|
|
328
|
+
- **Thinking** — the worker requests summarized thinking *display* without passing a `thinking`
|
|
329
|
+
option, so it never switches thinking on for a session that has it off, and never changes a budget.
|
|
330
|
+
|
|
331
|
+
### Session continuity
|
|
332
|
+
|
|
333
|
+
The native Agent SDK session id observed on the first turn is posted to
|
|
334
|
+
`POST /api/claude-native-session` and stored in `runtime_sessions.claude_native_session_id`. Later
|
|
335
|
+
turns pass it back as `resume`, so a Claude conversation survives worker restarts. The id is only
|
|
336
|
+
cached in the worker after the server accepts it, so a failed persist is retried next turn.
|
|
337
|
+
|
|
338
|
+
### Background tasks and workflow progress
|
|
339
|
+
|
|
340
|
+
The worker keeps one persistent CLI process per conversation, so background tasks survive the reply
|
|
341
|
+
that started them. The live task set is posted to `POST /api/background-tasks` (REPLACE semantics)
|
|
342
|
+
and broadcast as the `background_tasks` socket event; the composer's task panel renders it and can
|
|
343
|
+
stop an individual task over a `worker.control` push.
|
|
344
|
+
|
|
345
|
+
Tasks of type `local_workflow` carry an extra `workflowProgress` digest, because the SDK exposes no
|
|
346
|
+
workflow-progress surface — the worker reads it off disk:
|
|
347
|
+
|
|
348
|
+
- While the workflow runs, `subagents/workflows/<runId>/journal.jsonl` under the session directory
|
|
349
|
+
gives the agent set and their running/done state; agent labels come from the first line of each
|
|
350
|
+
`agent-<id>.jsonl`.
|
|
351
|
+
- The run record `workflows/<runId>.json` is written **only at completion**, and takes over as the
|
|
352
|
+
authoritative tree once it exists — it is the only source of phase titles, logs, token totals, and
|
|
353
|
+
per-agent tool counts.
|
|
354
|
+
- Parsing lives in `claude-worker/workflow-progress-digest.mjs` (`digestFromJournal`,
|
|
355
|
+
`digestFromRunRecord`), polling in `claude-session-process.mjs` (`syncWorkflowPoller`, 2 s, only
|
|
356
|
+
while such a task is live, published only when the digest actually changed). Reads are bounded by
|
|
357
|
+
byte caps and a filename pattern, and the digest is clamped (≤100 agents, ≤50 phases, ≤5 log lines,
|
|
358
|
+
per-field length caps). A run is joined to its task by the record's `taskId`, with a start-time
|
|
359
|
+
mtime window as the mid-run fallback.
|
|
360
|
+
- The relay never trusts the worker's JSON: `sanitizeWorkflowProgress` (`routes/messages-routes.mjs`)
|
|
361
|
+
re-applies every clamp before storing or broadcasting, and drops a digest with no phases and no
|
|
362
|
+
agents.
|
|
363
|
+
|
|
364
|
+
When a workflow settles, the worker buffers its final digest (at most five, terminal status never
|
|
365
|
+
downgraded) and attaches it to the next `POST /api/response` as `workflowRuns`. Runs that settled on
|
|
366
|
+
a journal snapshot get one more run-record read at drain time, since the CLI often flushes the record
|
|
367
|
+
a few seconds after the task disappears. The relay writes those rows into `workflow_runs` inside the
|
|
368
|
+
same transaction that finalizes the assistant message, keyed on the response message id, and returns
|
|
369
|
+
them with the message — so the transcript's collapsed **Finished background task** card (the same
|
|
370
|
+
tree renderer as the live panel, `buildWorkflowRunCard` in `public/app/background-tasks-view.mjs`)
|
|
371
|
+
survives a reload. A history rebuild deletes a conversation's `workflow_runs` alongside its
|
|
372
|
+
`subagent_runs`, because both key on message ids the rebuild replaces.
|
|
373
|
+
|
|
374
|
+
### Context usage
|
|
375
|
+
|
|
376
|
+
Claude sessions have no Copilot `events.jsonl` to tail. Instead the worker reads the live
|
|
377
|
+
context-window breakdown with `getContextUsage()` while the turn's `Query` is still open, and posts it
|
|
378
|
+
to `POST /api/claude-context-usage`; the relay stores it in `runtime_sessions.context_usage_json`.
|
|
379
|
+
`GET /api/context/:id` then serves Claude and Copilot sessions through one normalized `contextUsage`
|
|
380
|
+
view so a single renderer handles both. Claude supplies real per-category token counts; Copilot's
|
|
381
|
+
coarse system/tools + messages + buffer split is synthesized into the same shape and flagged
|
|
382
|
+
`isEstimate` when the runtime only reports a lower bound.
|
|
383
|
+
|
|
384
|
+
### Worker environment
|
|
385
|
+
|
|
386
|
+
`applyClaudeProviderEnvironment` sets these on the worker launch environment:
|
|
387
|
+
|
|
388
|
+
- `COPILOT_WEB_RELAY_WORKER_KIND=claude` — selects the Claude worker instead of the Copilot CLI
|
|
389
|
+
- `CLAUDE_RELAY_MODEL` — the conversation's model, used when a turn carries none
|
|
390
|
+
|
|
391
|
+
Also honored by the worker: `COPILOT_WORKSPACE_ROOT` (cwd), `COPILOT_WEB_RELAY_CONFIG` (relay config
|
|
392
|
+
path), `CLAUDE_CODE_EXECUTABLE` (explicit Claude Code binary), and
|
|
393
|
+
`COPILOT_WEB_RELAY_CLAUDE_WORKER_PATH` (override for the worker script location).
|
|
394
|
+
|
|
395
|
+
On Linux/macOS the Claude worker still launches inside a `tmux` session named after the SDK session
|
|
396
|
+
id, but without the `script`-based pseudo-TTY the Copilot workers need — it is a plain Node process.
|
|
397
|
+
On Windows it opens a `Claude Worker <id>` terminal window.
|
|
398
|
+
|
|
399
|
+
### Provider-aware usage snapshots
|
|
400
|
+
|
|
401
|
+
`fetchUsageSummary` reads GitHub Copilot plan quota, so it is skipped entirely for OpenAI and Claude
|
|
402
|
+
turns. Those replies carry no usage line rather than displaying Copilot premium-request numbers under
|
|
403
|
+
a reply that never touched Copilot.
|
|
404
|
+
|
|
405
|
+
## Cursor Agent SDK provider
|
|
406
|
+
|
|
407
|
+
Cursor conversations run through `server/cursor-worker/cursor-session-worker.mjs`, a per-conversation
|
|
408
|
+
Node worker like the Claude one, backed by the Cursor Agent SDK. Authentication uses a Cursor API key
|
|
409
|
+
saved via **Settings → Cursor SDK** (`POST /api/settings/cursor`); saving or replacing the key resets
|
|
410
|
+
and re-runs model discovery, which also records each model's supported reasoning-effort tiers
|
|
411
|
+
(`effortsByModel`).
|
|
412
|
+
|
|
413
|
+
Per-turn behavior:
|
|
414
|
+
|
|
415
|
+
| Relay mode | Cursor mapping |
|
|
416
|
+
| ----------- | ---------------------------------------------------------------------------- |
|
|
417
|
+
| `plan` | SDK native plan mode; a plan-shaped final message publishes a **plan_ready** board |
|
|
418
|
+
| `ask` / `autopilot` | The SDK's send options carry no per-turn instruction channel, so a `[Relay mode: …]` nudge is prepended to the user message — injected only when the mode changes, and a change away from a nudged mode sends an explicit cancellation |
|
|
419
|
+
| `agent` | Default behavior, no injection |
|
|
420
|
+
|
|
421
|
+
- **Model and reasoning effort are per turn.** Effort is validated against the model's discovered
|
|
422
|
+
tiers (`none` = model default); for `auto`/undiscovered models the request passes through and the
|
|
423
|
+
worker validates it against the resolved model's live params.
|
|
424
|
+
- **Stale agent handles are retried once.** A cached agent handle whose exchanged auth expired fails
|
|
425
|
+
as a terminal error result even though the API key is still valid, so the worker recreates the
|
|
426
|
+
handle and retries a single time; a second auth failure is treated as a real key problem.
|
|
427
|
+
- **Session root** — the worker keeps its per-session agent store at
|
|
428
|
+
`$CURSOR_AGENT_STORE_DIR` or `server/data/cursor-agents/<sdkSessionId>`, created on the first turn.
|
|
429
|
+
The explorer's Session root resolves to it via `resolveCursorSessionRoot`.
|
|
430
|
+
- Cursor turns are skipped by `fetchUsageSummary` like OpenAI and Claude turns — no Copilot usage
|
|
431
|
+
line is attached.
|
|
432
|
+
|
|
433
|
+
## Turn recovery and the max turn duration
|
|
434
|
+
|
|
435
|
+
A turn in `processing` is returned to the queue by two independent guards. **Both exempt a turn that
|
|
436
|
+
is blocked on an unanswered relay question** — that turn is waiting on a human, and `relay_questions`
|
|
437
|
+
carries its own expiry.
|
|
438
|
+
|
|
439
|
+
1. **Inactivity (`processingTimeoutMs`, default 10 min).** Measured from `owner_last_claimed_at`,
|
|
440
|
+
which every worker heartbeat refreshes for the message it is working on — *not* from when the turn
|
|
441
|
+
began. A turn that legitimately runs for hours of tool calls keeps pushing the cutoff forward.
|
|
442
|
+
`processing_at` is only the fallback for rows with no owner (session-worker routing disabled).
|
|
443
|
+
While the CLI is offline the window shrinks to 45 s.
|
|
444
|
+
2. **Absolute ceiling (Settings → Max turn duration, default 60 min).** Measured from `processing_at`.
|
|
445
|
+
`0` disables it; the slider range is 0–600 minutes in 5-minute steps. This is a backstop for a
|
|
446
|
+
worker that has hung while still heartbeating, and is stored in the DB app setting
|
|
447
|
+
`turn_ceiling_minutes` — not in `config.json`. Shared bounds and formatting live in
|
|
448
|
+
`shared/turn-ceiling.mjs`.
|
|
449
|
+
|
|
450
|
+
## Subagent runs and live streaming
|
|
451
|
+
|
|
452
|
+
- `POST /api/stream` and `POST /api/activity` / `POST /api/thought` accept an optional
|
|
453
|
+
`subagentRunId`. Frames carrying one are routed to that subagent's bubble instead of the parent.
|
|
454
|
+
- The browser renders a **live reply preview**: main-thread stream text is markdown-rendered into the
|
|
455
|
+
pending bubble while the turn runs, and replaced by the real message when it completes.
|
|
456
|
+
- Subagent bubbles nest by `parentSubagentId`. A child event arriving before its parent's bubble
|
|
457
|
+
exists builds the parent chain on demand, so nesting is preserved regardless of event order.
|
|
458
|
+
- After the turn finishes, `subagentRuns` are returned with the assistant message and re-rendered as
|
|
459
|
+
collapsible **🤖 <name>** sections containing that run's thoughts and activity.
|
|
460
|
+
- Reloading mid-turn repaints both the reply preview and every subagent bubble from persisted stream
|
|
461
|
+
rows (`deriveInFlightStreamTextByThread`).
|
|
462
|
+
- The Copilot extension propagates the SDK `agentId` onto question/tool activity, so subagent-issued
|
|
463
|
+
`ask_user` calls and tool results land in the right bubble. Re-discovering a known run no longer
|
|
464
|
+
demotes it to a root or renames it.
|
|
465
|
+
|
|
466
|
+
## Conversation list active-turn flag
|
|
467
|
+
|
|
468
|
+
`GET /api/conversations` returns `activeTurn` per conversation, derived from
|
|
469
|
+
`listConversationIdsWithActiveQueue` (`pending` / `processing` / `parked`). Clients previously tracked
|
|
470
|
+
this only from one-shot `message_status` socket events, which are lost if the socket drops between a
|
|
471
|
+
turn finishing and delivery — leaving a permanent spinner. `activeTurn: false` now clears the
|
|
472
|
+
client's optimistic state. Relay shutdown also waits 300 ms before disconnecting sockets so a terminal
|
|
473
|
+
`message_status` emitted just before an idle-deferred restart still reaches the browser.
|
|
474
|
+
|
|
475
|
+
## Image conversations (OpenAI BYOK)
|
|
476
|
+
|
|
477
|
+
Choosing **OpenAI Image (BYOK)** in New Chat creates a conversation whose replies are generated
|
|
478
|
+
images, calling the OpenAI Images API directly rather than going through a CLI worker.
|
|
479
|
+
|
|
480
|
+
- The New Chat dialog swaps **Reasoning effort** for **Quality**, and adds a **Size** picker whose
|
|
481
|
+
options depend on the model (`dall-e-2`: 256/512/1024 squares; `dall-e-3`: 1024 plus the two
|
|
482
|
+
1792 orientations; otherwise `auto` plus the 1024/1536 set). The chosen size is remembered per
|
|
483
|
+
browser and becomes the conversation's context tier.
|
|
484
|
+
- Generated images are stored per conversation and streamed from
|
|
485
|
+
`GET /api/generated-image/:conversationId/:messageId/:imageId/content`.
|
|
486
|
+
- **Edit this image** on a generated image sets it as the composer's image target (shown as an
|
|
487
|
+
*Editing …* chip); the next message is queued as an edit of that image instead of a fresh
|
|
488
|
+
generation. Image operations are recorded so a follow-up can continue from the right source image.
|
|
489
|
+
- Deleting a conversation garbage-collects its generated images.
|
|
490
|
+
- Continuity is gated by the `IMAGE_CONVERSATION_CONTINUITY_ENABLED` feature flag (on by default).
|
|
491
|
+
|
|
492
|
+
## Conversation sharing and per-message visibility
|
|
493
|
+
|
|
494
|
+
- **Share** on a conversation issues a read-only token; shared viewers read through
|
|
495
|
+
`GET /api/shared/:token` and its scoped upload/generated-image content routes.
|
|
496
|
+
- `PATCH /api/conversation/:id/message/:messageId/share-visibility` toggles a single message's
|
|
497
|
+
`hidden_from_shares` flag. Hidden messages are filtered out of every shared view while staying
|
|
498
|
+
fully visible — and visibly marked as hidden — to the owner.
|
|
499
|
+
- The control appears on message hover; already-hidden messages keep their label visible so the state
|
|
500
|
+
is never silently lost.
|
|
501
|
+
- `POST /api/shared/:token/presence` tracks shared-viewer presence, surfaced as a watcher count.
|
|
502
|
+
|
|
503
|
+
## Relay Mode Selection
|
|
504
|
+
|
|
505
|
+
The composer also includes a per-message mode picker:
|
|
506
|
+
|
|
507
|
+
- `plan`
|
|
508
|
+
- `ask`
|
|
509
|
+
- `agent`
|
|
510
|
+
- `autopilot`
|
|
511
|
+
|
|
512
|
+
Mode is stored with each queued message so the relay can change behavior per turn.
|
|
513
|
+
Clarification prompts from the CLI are forwarded back into the browser as question
|
|
514
|
+
cards with a reply box.
|
|
515
|
+
|
|
516
|
+
`plan` mode behavior:
|
|
517
|
+
- Draft a concise implementation plan only (no implementation output).
|
|
518
|
+
- Read-only inspection is allowed when it materially improves plan quality (for example `glob`, `rg`, `view`).
|
|
519
|
+
- Do not edit repository files or run mutating commands unless implementation is explicitly requested.
|
|
520
|
+
|
|
521
|
+
## Conversation titles
|
|
522
|
+
|
|
523
|
+
The active conversation header includes a `✍️` button for renaming the conversation inline.
|
|
524
|
+
Title edits are saved to the database and broadcast to other open clients immediately.
|
|
525
|
+
|
|
526
|
+
## Session worker rollout flags
|
|
527
|
+
|
|
528
|
+
Session-worker refactor gates are OFF by default and can be enabled per flag:
|
|
529
|
+
|
|
530
|
+
- `SESSION_WORKER_ROUTING_ENABLED`
|
|
531
|
+
- `SESSION_WORKER_CONTINUATION_ROUTING_ENABLED`
|
|
532
|
+
- `SESSION_WORKER_FALLBACK_RESTART_ENABLED`
|
|
533
|
+
|
|
534
|
+
Other flags:
|
|
535
|
+
|
|
536
|
+
- `IMAGE_CONVERSATION_CONTINUITY_ENABLED` — ON by default; keeps generated-image context across turns
|
|
537
|
+
so *Edit this image* can continue from the right source image.
|
|
538
|
+
|
|
539
|
+
Configuration precedence is:
|
|
540
|
+
|
|
541
|
+
1. `server/config.json` → `features.{FLAG_NAME}` (`true/false`, `1/0`, `yes/no`, `on/off`)
|
|
542
|
+
2. Environment override: `COPILOT_REMOTE_{FLAG_NAME}`
|
|
543
|
+
|
|
544
|
+
Unknown flag names and invalid values are ignored safely.
|
|
545
|
+
|
|
546
|
+
Question bridge rule:
|
|
547
|
+
|
|
548
|
+
- User-facing questions/clarifications must use `ask_user` so they flow through
|
|
549
|
+
the relay question bridge and render as web question cards/buttons.
|
|
550
|
+
- This applies in `autopilot` too: still call `ask_user` for clarification, and
|
|
551
|
+
the relay layer will surface the question card even if the direct question hook
|
|
552
|
+
is bypassed.
|
|
553
|
+
- Plain-text assistant questions are not considered bridge-backed questions, and there is
|
|
554
|
+
**no fallback that converts them into question cards**. Every relay question originates from an
|
|
555
|
+
explicit request — `onUserInputRequest`, `onElicitationRequest`, or Claude's `AskUserQuestion`.
|
|
556
|
+
A turn that merely ends with a plain-text question simply completes, and the user answers it as
|
|
557
|
+
the next message.
|
|
558
|
+
|
|
559
|
+
## Session Worker Management
|
|
560
|
+
|
|
561
|
+
Session workers are long-running CLI processes that execute turns for Copilot agent sessions. The relay manages a pool of workers to enable:
|
|
562
|
+
- Parallel execution (multiple sessions can run simultaneously)
|
|
563
|
+
- Session persistence (a worker stays alive across turns for a given session)
|
|
564
|
+
- Graceful restarts (degraded workers are replaced without losing queued work)
|
|
565
|
+
|
|
566
|
+
### Worker Lifecycle States
|
|
567
|
+
|
|
568
|
+
1. **Spawned** — Worker process created, initializing CLI connection
|
|
569
|
+
2. **Ready** — Worker opened WebSocket to relay, registered in worker pool, ready to accept turns
|
|
570
|
+
3. **Processing** — Worker actively executing a turn (streaming activity to browser)
|
|
571
|
+
4. **Degraded** — Worker failed heartbeat or reported errors; relay moves pending work to other workers or restarts
|
|
572
|
+
5. **Dead/Exited** — Worker process terminated; polling loop will spawn replacement if turns still pending
|
|
573
|
+
|
|
574
|
+
### Worker Heartbeat & Health Check
|
|
575
|
+
|
|
576
|
+
- Workers send keep-alive pings every 30 seconds (default) to `/api/workers/{id}/heartbeat`
|
|
577
|
+
- Relay tracks last heartbeat time for each worker
|
|
578
|
+
- If a worker misses 2+ consecutive heartbeats (~60 seconds), it's marked "degraded"
|
|
579
|
+
- Degraded workers don't receive new turns; existing work is reassigned
|
|
580
|
+
- Polling loop detects degraded workers and respawns them if they're still needed
|
|
581
|
+
|
|
582
|
+
### Supervisor Service
|
|
583
|
+
|
|
584
|
+
The **session-worker-supervisor-service** tracks all active workers:
|
|
585
|
+
|
|
586
|
+
- Maintains registry: `workers[sdkSessionId] = { id, process, sdkSessionId, status, lastHeartbeat }`
|
|
587
|
+
- Monitors process health: checks if process is still running, collects exit codes
|
|
588
|
+
- Handles restarts: when a worker fails, spawns a replacement with exponential backoff
|
|
589
|
+
- Cleans up: removes dead workers from registry after TTL (default 5 minutes)
|
|
590
|
+
|
|
591
|
+
### Environment & Workspace
|
|
592
|
+
|
|
593
|
+
Session workers inherit:
|
|
594
|
+
|
|
595
|
+
- `COPILOT_WORKSPACE_ROOT` — Target workspace directory (from turn metadata or session config)
|
|
596
|
+
- `INIT_CWD` — Initial working directory (fallback for workspace resolution)
|
|
597
|
+
- `COPILOT_REMOTE_*` — All relay config via environment variables
|
|
598
|
+
- CLI session state from the last known good checkpoint
|
|
599
|
+
|
|
600
|
+
### Restart Orchestration
|
|
601
|
+
|
|
602
|
+
When a worker needs restart:
|
|
603
|
+
|
|
604
|
+
1. **Graceful shutdown** — Send SIGTERM, wait up to 5 seconds for clean exit
|
|
605
|
+
2. **Kill if needed** — Force SIGKILL if SIGTERM timeout
|
|
606
|
+
3. **Backoff** — Wait before respawn (start at 1s, double up to 30s max)
|
|
607
|
+
4. **Reassign work** — Any pending turns for that session are placed back in queue for the new worker
|
|
608
|
+
5. **Report status** — Relay broadcasts worker status to browser UI (shows "reconnecting..." briefly)
|
|
609
|
+
|
|
610
|
+
### Platform-Specific Behavior
|
|
611
|
+
|
|
612
|
+
- **macOS/Linux:** Workers prefer `tmux` sessions for better isolation and debugging (can attach with `tmux attach -t {sessionId}`)
|
|
613
|
+
- **Windows:** Workers launch in a new Terminal window (TTY-Console if available, otherwise cmd)
|
|
614
|
+
- Both: Worker inherits relay's environment and runs from relay repo root so project-local extensions are discoverable
|
|
615
|
+
|
|
616
|
+
## How It Works
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
[Browser] ←── WebSocket ──→ [server.js :3333] ←── WebSocket-first relay ──→ [Copilot CLI session]
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
1. Browser sends a message → stored in server queue
|
|
623
|
+
2. The relay worker socket delivers queued turns to the bound CLI session immediately
|
|
624
|
+
3. The CLI processes the turn and posts the response to `POST /api/response`
|
|
625
|
+
4. Server broadcasts response via Socket.io → appears in browser instantly
|
|
626
|
+
|
|
627
|
+
## Monitoring Mode (CLI)
|
|
628
|
+
|
|
629
|
+
After launching the server, the CLI opens a worker WebSocket link:
|
|
630
|
+
- Identifies itself with the bound SDK session id and worker pid
|
|
631
|
+
- Receives queued turns over `ws(s)://.../api/session-worker/ws`
|
|
632
|
+
- Reconnects starting at 1 second, with exponential backoff capped at 8 seconds
|
|
633
|
+
- Falls back to `GET /api/pending` only while the worker socket is unavailable
|
|
634
|
+
- Processes any message with full PC access (file system, commands, etc.)
|
|
635
|
+
- Posts the response back
|
|
636
|
+
- CLI appears **online** (green dot) in the web UI while the relay is connected
|
|
637
|
+
- While working, relay tool activity is streamed into the pending assistant bubble
|
|
638
|
+
(for example `Search (glob)` and `Search (grep)`).
|
|
639
|
+
- Assistant reply text is streamed into the pending assistant bubble while the turn is running.
|
|
640
|
+
- Tool activity is now also kept with the assistant message as a collapsible
|
|
641
|
+
**Tool activity** section after the response arrives.
|
|
642
|
+
- Clarification prompts from `ask_user`/user-input requests are forwarded as
|
|
643
|
+
question cards in the conversation; answering the card resumes the waiting turn.
|
|
644
|
+
- Answered relay question cards stay visible in the conversation journal, including
|
|
645
|
+
the answer you submitted.
|
|
646
|
+
|
|
647
|
+
### Session activation behavior (important)
|
|
648
|
+
|
|
649
|
+
In extension-managed mode, the web server can already be running while the CLI is still
|
|
650
|
+
shown as offline. The worker socket starts when the Copilot session becomes active
|
|
651
|
+
(`onSessionStart`), which usually happens after your first prompt in the CLI.
|
|
652
|
+
|
|
653
|
+
This means the following startup sequence is expected:
|
|
654
|
+
1. Open `http://localhost:3333` and briefly see "CLI is offline"
|
|
655
|
+
2. Send one message in the CLI
|
|
656
|
+
3. Extension opens the worker socket and queued web messages begin processing
|
|
657
|
+
|
|
658
|
+
If a conversation is still unbound when the CLI first sees it, the extension now claims it through `/api/session-sync` before processing so the browser can send back to the same SDK session. A separate startup sync to `/api/session-workspace-root` reports the CLI working directory for the already-bound session; it no longer creates a placeholder conversation on its own.
|
|
659
|
+
|
|
660
|
+
| Symptom | Check |
|
|
661
|
+
|---|---|
|
|
662
|
+
| UI says "CLI is offline" | Verify `/api/status` works with the auth cookie or `Authorization: Bearer <token>` and shows `cliOnline: true` |
|
|
663
|
+
| UI flaps online/offline after restart | Ensure you are not mixing extension-managed mode with standalone `relay.mjs`, and confirm only one relay process owns port `3333` |
|
|
664
|
+
| "Web relay connected" banner repeats too often | Ensure only one extension instance is active; banner dedupe now persists across extension restarts for the same CLI session and reprints only when relay details change |
|
|
665
|
+
| No response after sending | Tail `server\ext-debug.log` and confirm `onSessionStart fired` + `startPolling called` |
|
|
666
|
+
| Wrong model used | Check logs for `Model selected: requested=... active=...` |
|
|
667
|
+
| Question card stuck | Answer in the card UI; logs should show `relay question created` and `relay question answered` |
|
|
668
|
+
| Tunnel not connecting | Check server console for `[ssh-tunnel]` lines; confirm SSH key auth works without password |
|
|
669
|
+
| Tunnel keeps reconnecting | VPS `sshd_config` needs `GatewayPorts no` (default) — Caddy handles the public port |
|
|
670
|
+
|
|
671
|
+
## Windows-Specific Runtime Behavior
|
|
672
|
+
|
|
673
|
+
### Worker Launcher
|
|
674
|
+
|
|
675
|
+
- Workers on Windows launch via a **stable Terminal window** (when available).
|
|
676
|
+
- If TTY-Console (optional dependency) is installed, workers use an interactive terminal for debugging.
|
|
677
|
+
- Otherwise, workers spawn in a new `cmd` or PowerShell window.
|
|
678
|
+
- Window name is workspace-based so repeated worker restarts reuse the same window instead of opening duplicates.
|
|
679
|
+
- Claude workers open a `Claude Worker <id>` window running `node claude-session-worker.mjs` directly, instead of the `gh copilot` launch path.
|
|
680
|
+
|
|
681
|
+
### Path Handling
|
|
682
|
+
|
|
683
|
+
- **Drives API** — `/api/drives/roots` returns fixed/removable drive letters (`C:`, `D:`, etc.) instead of mount points.
|
|
684
|
+
- **Path format** — `C:/Users/alice/project/file.txt` (forward slashes, drive letter prefix).
|
|
685
|
+
- **Directory listing** — Uses PowerShell via `Get-ChildItem` for recursive listings (faster than fs.readdir on large directories).
|
|
686
|
+
- **Workspace root** — Must be an absolute Windows path; relative paths or UNC paths (`\\server\share`) are handled but may behave unexpectedly.
|
|
687
|
+
|
|
688
|
+
### Model Refresh
|
|
689
|
+
|
|
690
|
+
- Model discovery on Windows invokes `copilot.ps1` via PowerShell with `-ExecutionPolicy Bypass` to bypass execution policy restrictions.
|
|
691
|
+
- Command: `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "copilot help config | Select-String -Pattern 'gpt'"`.
|
|
692
|
+
- If PowerShell is not available, falls back to direct `copilot help config` (may fail due to execution policy).
|
|
693
|
+
|
|
694
|
+
### Environment Variables
|
|
695
|
+
|
|
696
|
+
- Workers inherit all relay server environment, including `COPILOT_WORKSPACE_ROOT` and `INIT_CWD`.
|
|
697
|
+
- On Windows, these paths use backslash or forward-slash format; CLI normalize paths automatically.
|
|
698
|
+
- `PYTHONUNBUFFERED=1` and `NODE_OPTIONS` should be set before relay startup for worker inheritance.
|
|
699
|
+
|
|
700
|
+
### SSH Tunnel on Windows
|
|
701
|
+
|
|
702
|
+
- SSH tunnel uses `ssh.exe` (assumes `git-bash` or OpenSSH is in PATH).
|
|
703
|
+
- If SSH is not available, tunnel mode cannot be "managed"; stays in "disabled" mode.
|
|
704
|
+
- Windows Firewall may require inbound rule for local relay port (default 3333) if browser is on another machine.
|
|
705
|
+
|
|
706
|
+
## API Reference
|
|
707
|
+
|
|
708
|
+
All authenticated routes accept an HttpOnly auth cookie or an `Authorization: Bearer <token>` header.
|
|
709
|
+
|
|
710
|
+
`GET /api/status` now also includes `readyBanner`, a preformatted relay-info payload used by the CLI extension to print the access window directly in the Copilot CLI client when relay connectivity is established.
|
|
711
|
+
It also includes `restartOrchestrator` with the current relay-side restart transaction state.
|
|
712
|
+
It also includes `relayShutdown`, which reports queued manual relay shutdown/restart state separately from the worker restart orchestrator.
|
|
713
|
+
Queue metrics include `parkedCount` for turns deferred behind restart/rebind gates.
|
|
714
|
+
|
|
715
|
+
| Method | Path | Description |
|
|
716
|
+
|--------|------|-------------|
|
|
717
|
+
| POST | `/api/message` | Send a message from the browser |
|
|
718
|
+
| POST | `/api/upload` | Upload binary file content (deduped by SHA-256) |
|
|
719
|
+
| GET | `/api/upload/:sha256/content` | Stream stored upload content by hash |
|
|
720
|
+
| PATCH | `/api/conversation/:id/draft` | Save composer draft text and optional `draftAttachments` |
|
|
721
|
+
| GET | `/api/files/*` | Stream a workspace file by repo-relative path (token required) |
|
|
722
|
+
| GET | `/api/files-preview/*` | Return structured preview JSON for markdown/code/text/image/video files |
|
|
723
|
+
| GET | `/api/repo/tree` | Return repository root + first-level entries for lazy workspace browsing |
|
|
724
|
+
| GET | `/api/repo/list` | Lazy-load workspace directory entries (`path`, `includeHidden`, `includeHeavy`) |
|
|
725
|
+
| GET | `/api/drives/roots` | Return browsable root(s) for explorer drive mode — on Windows returns fixed/removable drive letters; on Linux returns a single `/` root node |
|
|
726
|
+
| GET | `/api/drives/list` | Lazy-load directory entries (`path`, optional `includeHidden`) — on Windows `path` is a drive-letter path (e.g. `C:/foo`); on Linux `path` is an absolute POSIX path (e.g. `/home/user`) |
|
|
727
|
+
| GET | `/api/session-root/list` | List the explorer's Session root (`path`, optional `includeHidden`) — like `/api/drives/list`, but a not-yet-created root returns an empty folder (`exists: false`) instead of 404, and the sibling `<path>.jsonl` transcript is appended as a child |
|
|
728
|
+
| GET | `/api/drives/file` | Stream a file by path — Windows drive path or Linux absolute path depending on server platform |
|
|
729
|
+
| GET | `/api/drives/files-preview` | Return structured preview JSON for a file — Windows drive path or Linux absolute path depending on server platform |
|
|
730
|
+
| GET | `/api/git/status` | Git status for the conversation's workspace root (`branch`, `upstream`, `ahead`, `behind`, `files[]` incl. untracked); a non-repo root returns `isRepo: false` rather than an error |
|
|
731
|
+
| GET | `/api/git/diff` | Full-context unified diff for one changed file (`path`, optional `untracked=1`); the client renders both "changes only" and "full file" views from this single patch |
|
|
732
|
+
| POST | `/api/git/pull` | Run `git pull` in the conversation's workspace root and return the combined output |
|
|
733
|
+
| GET | `/api/conversations` | List all conversations |
|
|
734
|
+
| GET | `/api/sessions` | List runtime sessions bound 1:1 to conversations |
|
|
735
|
+
| GET | `/api/conversation/:id` | Get conversation message windows (`before*`, `after*`, or `aroundMessageId`) plus session-root metadata |
|
|
736
|
+
| GET | `/api/search/messages` | Search message text across all conversations (`q`, `limit`, `offset`) |
|
|
737
|
+
| PATCH | `/api/conversation/:id` | Update a conversation title |
|
|
738
|
+
| POST | `/api/conversation/:id/compact` | Compact a conversation into a new one with carry-over summary seed |
|
|
739
|
+
| DELETE | `/api/conversation/:id` | Delete a conversation |
|
|
740
|
+
| GET | `/api/sdk-session-delete/pending` | (CLI relay) Fetch next pending SDK session delete request |
|
|
741
|
+
| POST | `/api/sdk-session-delete/result` | (CLI relay) Report SDK session delete result |
|
|
742
|
+
| POST | `/api/session-sync` | (CLI relay) Sync conversation↔SDK binding and optionally confirm orchestrator rebind completion |
|
|
743
|
+
| POST | `/api/session-workspace-root` | (CLI relay) Report the startup workspace CWD for a session once it is known |
|
|
744
|
+
| GET | `/api/pending` | (CLI) Fetch next pending message |
|
|
745
|
+
| POST | `/api/response` | (CLI) Submit response for a message |
|
|
746
|
+
| GET | `/api/restart-orchestrator` | Read relay restart orchestrator state |
|
|
747
|
+
| POST | `/api/restart-orchestrator/request` | Queue a restart transaction for a target SDK session |
|
|
748
|
+
| POST | `/api/activity` | (CLI) Push in-flight tool activity for current message; web search/fetch entries may include bounded queries, URLs, and output previews, while `store_memory`/`vote_memory` entries include available memory metadata |
|
|
749
|
+
| POST | `/api/stream` | (CLI) Push in-flight assistant text stream for current pending message |
|
|
750
|
+
| POST | `/api/relay/pause` | Pause dequeueing and drop currently queued messages |
|
|
751
|
+
| POST | `/api/relay/resume` | Resume dequeueing after pause |
|
|
752
|
+
| POST | `/api/relay/shutdown` | Queue a localhost-only authenticated relay shutdown or self-restart |
|
|
753
|
+
| GET | `/api/relay-questions` | (CLI/UI) List relay questions by `status` (for example `pending` or `answered`) |
|
|
754
|
+
| GET | `/api/relay-question/:id` | (CLI) Fetch a single relay question |
|
|
755
|
+
| POST | `/api/relay-question` | (CLI) Create a relay question for the browser |
|
|
756
|
+
| POST | `/api/relay-question/:id/answer` | (UI) Submit an answer for a relay question |
|
|
757
|
+
| POST | `/api/relay-question/:id/timeout` | (CLI/UI) Mark a relay question timed out |
|
|
758
|
+
| POST | `/api/heartbeat` | (CLI) Keep CLI status alive |
|
|
759
|
+
| GET | `/api/status` | Overall status |
|
|
760
|
+
| GET | `/api/models` | Live/cached model catalog used by the UI picker |
|
|
761
|
+
| POST | `/api/models/snapshot` | (CLI/relay) Publish discovered model snapshot |
|
|
762
|
+
| POST | `/api/conversation/:id/refresh-history` | Re-import an existing conversation's history through the local Copilot SDK |
|
|
763
|
+
| GET | `/api/context/:conversationId` | Context metrics for a conversation or `sdk_session_id`. Copilot sessions are parsed from session-state events (falling back to a labeled lower-bound completion-token estimate when full legacy buckets are missing); Claude sessions are served from the breakdown their worker stored. Both return the same normalized `contextUsage` view alongside `providerType` and the runtime's own `text` dump |
|
|
764
|
+
| GET | `/api/context` | Same payload when a `conversationId` query is provided; otherwise returns a missing-selection response |
|
|
765
|
+
| GET | `/api/usage` | Unified plan usage: one card per configured provider (`providers[]` with `meters`, `details`, `links`) built from the live Copilot quota, the Claude worker's latest stored snapshot, and the derived Cursor cycle totals. A provider that cannot be read yields an unavailable card rather than failing the response, so the modal always renders. Legacy top-level Copilot quota fields are spread alongside `providers` for cached clients; `?legacy=1` returns the Copilot-only snapshot on its own |
|
|
766
|
+
| GET | `/api/settings/cursor-allowance` | Read the manual Cursor plan allowances (`cursorModelsUsd`, `otherModelsUsd`, `resetDay`) |
|
|
767
|
+
| POST | `/api/settings/cursor-allowance` | Set the Cursor pool allowances and billing reset day; `null` clears an allowance, and `resetAccounting: true` re-baselines the derived spend tracking |
|
|
768
|
+
| GET | `/api/settings/claude` | Read Claude provider settings (`enabled`, `model`, `models`, `availableModels`) |
|
|
769
|
+
| POST | `/api/settings/claude` | Enable/disable Claude, set the default model or the enabled model subset; triggers discovery and unstarted-conversation reconciliation |
|
|
770
|
+
| GET | `/api/settings/cursor` | Read Cursor provider settings — same shape as Claude's: `models` is the enabled subset, `availableModels` the full discovered list, plus per-model `efforts` |
|
|
771
|
+
| POST | `/api/settings/cursor` | Set/remove the Cursor API key, default model, or enabled model subset; key changes reset discovered models and effort tiers |
|
|
772
|
+
| GET | `/api/settings/turn-ceiling` | Read the max turn duration plus slider bounds (`minMinutes`, `maxMinutes`, `stepMinutes`, `defaultMinutes`) |
|
|
773
|
+
| POST | `/api/settings/turn-ceiling` | Set the max turn duration in minutes (`0` = no limit) |
|
|
774
|
+
| POST | `/api/claude-native-session` | (Claude worker) Persist the native Agent SDK session id for resume |
|
|
775
|
+
| POST | `/api/claude-context-usage` | (Claude worker) Report the session's context-window breakdown after a turn |
|
|
776
|
+
| POST | `/api/claude-plan-usage` | (Claude worker) Report the session's structured `/usage` data — plan rate-limit windows plus session cost totals — falling back to the stable `modelUsage`/`totalCostUsd` result fields. Stored as the latest Claude snapshot |
|
|
777
|
+
| POST | `/api/cursor-plan-usage` | (Cursor worker) Report the agent's cumulative billed usage; the relay diffs it against a per-agent checkpoint and books the increase into the current billing cycle under the pool implied by the turn's model |
|
|
778
|
+
| POST | `/api/subagent-run` | (Worker) Register/update a subagent run for the active turn |
|
|
779
|
+
| POST | `/api/conversation/:conversationId/subagent/:subagentRunId/cancel` | Request cancellation of one subagent run (unsupported by the Claude runtime) |
|
|
780
|
+
| PATCH | `/api/conversation/:id/message/:messageId/share-visibility` | Hide or unhide a single message from shared views |
|
|
781
|
+
| POST | `/api/conversation/:id/share` | Create or update the conversation's read-only share token |
|
|
782
|
+
| GET | `/api/shared/:token` | Read-only shared conversation view (hidden messages filtered out) |
|
|
783
|
+
| POST | `/api/shared/:token/presence` | Report shared-viewer presence for the watcher count |
|
|
784
|
+
| POST | `/api/openai/images/generate` | Generate images for an OpenAI Image conversation |
|
|
785
|
+
| POST | `/api/image-operations/:operationId/execute` | Execute a queued image generation/edit operation |
|
|
786
|
+
| GET | `/api/generated-image/:conversationId/:messageId/:imageId/content` | Stream a generated image blob |
|
|
787
|
+
|
|
788
|
+
### Manual relay shutdown / self-restart
|
|
789
|
+
|
|
790
|
+
- `POST /api/relay/shutdown` is localhost-only even when the relay is otherwise reachable on the network.
|
|
791
|
+
- The request body accepts:
|
|
792
|
+
- `reason` (optional string)
|
|
793
|
+
- `requestedBy` (optional string)
|
|
794
|
+
- `restart` (optional boolean-ish flag; `true` queues self-restart instead of plain shutdown)
|
|
795
|
+
- Example restart request:
|
|
796
|
+
|
|
797
|
+
```json
|
|
798
|
+
{
|
|
799
|
+
"reason": "manual-restart",
|
|
800
|
+
"requestedBy": "localhost-api",
|
|
801
|
+
"restart": true
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
- Response payload includes:
|
|
806
|
+
- `status`: `queued` or `shutting_down`
|
|
807
|
+
- `action`: `shutdown` or `restart`
|
|
808
|
+
- `restart`: boolean mirror of the action
|
|
809
|
+
- `requestedAt`, `reason`, `requestedBy`
|
|
810
|
+
- `queue`: current `pendingCount` / `processingCount` / `parkedCount`
|
|
811
|
+
- The relay waits until the queue is idle before acting; this API is not an interrupt or cancel mechanism.
|
|
812
|
+
- Ownership after an intentional restart follows the argv role, so exactly one supervisor acts on exit 75:
|
|
813
|
+
- `server.js --supervised` exits 75 and relaunches under `.github/extensions/web-relay/server-lifecycle/managed-server.mjs`
|
|
814
|
+
- bare `node server/server.js` keeps its supervisor attached, respawns the `--relay-runtime` child, and keeps the same terminal session alive
|
|
815
|
+
|
|
816
|
+
### Upload storage model
|
|
817
|
+
|
|
818
|
+
- Physical blobs are stored in `server/uploads/<sha256>` (content-addressed).
|
|
819
|
+
- Metadata (`original_name`, `mime_type`, `size_bytes`) is stored in SQLite.
|
|
820
|
+
- Message/conversation references are tracked in SQLite; when a conversation is deleted,
|
|
821
|
+
unreferenced blobs are garbage-collected from disk automatically.
|
|
822
|
+
- Image attachments are forwarded to the Copilot SDK as multimodal attachments
|
|
823
|
+
(`file` when a disk path is available, otherwise inline `blob`).
|
|
824
|
+
- Non-image uploads continue to be exposed as file references.
|
|
825
|
+
- `POST /api/upload` sniffs the payload's magic bytes and reconciles them against the
|
|
826
|
+
client-supplied `X-File-Type` header. The detected type wins on disagreement, so a
|
|
827
|
+
disguised executable is stored as what it actually is. Unrecognised bytes (plain text,
|
|
828
|
+
CSV, source files) keep the claimed type. The response reports `mimeTypeCorrected`.
|
|
829
|
+
|
|
830
|
+
### Composer attachments: paste, drag-and-drop and eager upload
|
|
831
|
+
|
|
832
|
+
- Files reach the composer through three paths, all sharing one ingestion function:
|
|
833
|
+
the 📎 picker, `Ctrl+V` paste, and drag-and-drop onto the input area.
|
|
834
|
+
- **Paste attaches, it never sends.** When the clipboard holds both an image and text,
|
|
835
|
+
the image wins and the text is discarded. When it holds only text, the handler does
|
|
836
|
+
nothing so the browser's native paste is untouched.
|
|
837
|
+
- Clipboard bitmaps arrive unnamed (or as a generic `image.png`) and are renamed
|
|
838
|
+
`pasted-<ISO-timestamp>.<ext>`.
|
|
839
|
+
- Images over ~2 MB are downscaled client-side to a 2560 px longest edge and re-encoded
|
|
840
|
+
to **JPEG** before upload. GIF and SVG are never re-encoded, and a re-encode that fails,
|
|
841
|
+
grows the file, or yields anything other than JPEG/PNG falls back to the original bytes.
|
|
842
|
+
- The output format is deliberately restricted to JPEG/PNG: Copilot's image-input docs call
|
|
843
|
+
these "the most widely supported formats". WebP compresses better and previews correctly
|
|
844
|
+
in the browser, but arrives at the model unreadable.
|
|
845
|
+
- Re-encoding therefore has two triggers, tracked by `reencodeReason()`:
|
|
846
|
+
- `'size'` — the image is over the byte threshold. Abandoned if the result is no smaller.
|
|
847
|
+
- `'format'` — the source is not PNG/JPEG/GIF (e.g. WebP, AVIF, HEIC), so it is converted
|
|
848
|
+
at its original dimensions **regardless of size**, and kept even if the result is larger:
|
|
849
|
+
a bigger image the model can read beats a smaller one it cannot.
|
|
850
|
+
- Without the `'format'` trigger a WebP small enough to skip the size threshold would reach
|
|
851
|
+
the model invisible.
|
|
852
|
+
- Attachments upload **eagerly** on selection rather than on Send. Chips show an
|
|
853
|
+
uploading spinner, and a failed upload becomes a clickable retry.
|
|
854
|
+
- Send is disabled while any upload is in flight. Failed uploads are deliberately not
|
|
855
|
+
counted as in-flight, so a failure can never wedge the button.
|
|
856
|
+
- Exceeding the 6-attachment cap keeps the first 6 and reports the overflow in a toast
|
|
857
|
+
(previously the overflow was dropped silently).
|
|
858
|
+
- Shared read-only viewers cannot paste or drop.
|
|
859
|
+
|
|
860
|
+
### Composer attachment cache
|
|
861
|
+
|
|
862
|
+
- Pending attachments persist **per conversation**, mirroring how text drafts already work.
|
|
863
|
+
Switching conversations and returning restores them, as does reloading the page.
|
|
864
|
+
- Storage is `conversations.draft_attachments`: a JSON array of `{sha256, name, type, size}`.
|
|
865
|
+
Only already-uploaded attachments are persisted — never file bytes.
|
|
866
|
+
- `PATCH /api/conversation/:id/draft` accepts an optional `draftAttachments` array.
|
|
867
|
+
Omitting the field leaves the stored attachments untouched; passing `null` or `[]` clears
|
|
868
|
+
them. Every entry must reference a blob the server already holds, otherwise the request
|
|
869
|
+
is rejected with a 400.
|
|
870
|
+
- Removing an attachment is expressed as a draft save carrying the remaining list, so it is
|
|
871
|
+
ordered against in-flight draft writes rather than racing them. The server diffs the list
|
|
872
|
+
and releases the reference itself; there is deliberately no delete-by-hash endpoint, since
|
|
873
|
+
blobs are content-addressed and shared, and such a route would let one caller destroy
|
|
874
|
+
another conversation's freshly uploaded blob.
|
|
875
|
+
- Draft blobs are kept alive by an `upload_refs` row using the sentinel
|
|
876
|
+
`message_id = '__draft__'`. On send, the real message reference is inserted and the
|
|
877
|
+
sentinel row is dropped. Reference release only reclaims blobs whose sentinel row this
|
|
878
|
+
conversation actually held.
|
|
879
|
+
- Conversation deletion needs no special handling: the existing foreign-key cascade and
|
|
880
|
+
orphan sweep already collect draft blobs.
|
|
881
|
+
- Because uploads are eager, a blob can exist before anything references it (pasting into a
|
|
882
|
+
conversation that is then abandoned, or switching conversations mid-upload). A sweep runs
|
|
883
|
+
at startup and every 6 hours to reclaim unreferenced blobs older than 24 hours; the age
|
|
884
|
+
floor guarantees an in-flight upload is never collected.
|
|
885
|
+
- Attachments added before a conversation exists stay in the composer (already uploaded) and
|
|
886
|
+
are adopted by the conversation created on first send.
|
|
887
|
+
|
|
888
|
+
### Chat file reference tokens
|
|
889
|
+
|
|
890
|
+
- The web explorer/file preview can copy references as backticked tokens:
|
|
891
|
+
- ``@file:<path>``
|
|
892
|
+
- ``@folder:<path>``
|
|
893
|
+
- Folder tokens use the full folder path.
|
|
894
|
+
- File tokens use the full file path shown by the preview/browser context.
|
|
895
|
+
- Tokens are root-agnostic: workspace paths are repo-relative, drive paths keep `C:/...` form.
|
|
896
|
+
|
|
897
|
+
### Reference-driven inspection helper
|
|
898
|
+
|
|
899
|
+
- Messages containing `@file:` tokens are parsed server-side before queueing.
|
|
900
|
+
- Text/markdown references stay as plain references (no binary payload attached).
|
|
901
|
+
- Small image references (up to ~1 MB) are attached to the pending turn so vision-capable
|
|
902
|
+
models receive real image input.
|
|
903
|
+
- Oversized image references are left as plain text references to avoid oversized request payloads.
|
|
904
|
+
|
|
905
|
+
### Workspace file bridge
|
|
906
|
+
|
|
907
|
+
- Use `/api/files/<repo-relative-path>` to open workspace files in a new tab. The browser sends the auth cookie automatically.
|
|
908
|
+
- Use `/api/files-preview/<repo-relative-path>` for structured preview JSON (`kind`, `language`, `content`, `truncated`, `size`).
|
|
909
|
+
- Use `/api/repo/tree?includeHidden=0&includeHeavy=0` to load workspace root and first-level entries.
|
|
910
|
+
- Use `/api/repo/list?path=<repo-relative-dir>&includeHidden=0|1&includeHeavy=0|1` for lazy-loaded workspace directory browsing.
|
|
911
|
+
- Use `/api/drives/roots` + `/api/drives/list?path=<path>&includeHidden=0|1` for lazy-loaded drive/root browsing (separate from workspace heavy mode).
|
|
912
|
+
- Use `/api/drives/file?path=<path>` and `/api/drives/files-preview?path=<path>` for drive/root file raw/preview access.
|
|
913
|
+
- Use `/api/session-root/list?path=<sessionRootPath>&includeHidden=0|1` for the explorer's Session root. Child directories below it lazy-load through `/api/drives/list` as usual. The Claude Agent SDK creates a session's directory lazily — only once the session writes `subagents/` or `tool-results/` files — so the root is served as an empty folder until then rather than 404ing, and the transcript that lives one level up in the project directory is listed as a child of it. Cursor sessions resolve their Session root the same lazy way: the worker's per-session agent store (`$CURSOR_AGENT_STORE_DIR` or `server/data/cursor-agents/<sdkSessionId>`) exists only after the session's first turn, and the Session button stays disabled until it does.
|
|
914
|
+
- Requests are auth-protected and restricted to files inside the workspace root.
|
|
915
|
+
- **Platform split** — the drives API adapts automatically based on the server OS:
|
|
916
|
+
- **Windows:** drive roots are discovered via `fsutil.exe`; paths use Windows drive-letter format (`C:/foo/bar`); directory listing uses PowerShell.
|
|
917
|
+
- **Linux:** a single `/` root is returned; paths are absolute POSIX paths (`/home/user/file.txt`); directory listing uses `fs.readdir` + `fs.stat`.
|
|
918
|
+
- The `/api/status` response includes a `platform` field (`win32`, `linux`, `darwin`, etc.) so the browser UI can adapt path handling accordingly.
|
|
919
|
+
- Drive browsing is auth-protected; path traversal attacks and non-absolute paths are rejected on both platforms.
|
|
920
|
+
- Traversal or non-file paths are rejected.
|
|
921
|
+
- **Linux access scope** — on Linux the drives API currently allows any authenticated user to browse and read any path that the server process has OS-level read access to (i.e. the same permissions as the user running the relay). There is no additional allowlist restriction beyond requiring an absolute path.
|
|
922
|
+
> **TODO:** add an optional `drivesAllowList` config key (array of absolute path prefixes) so operators can restrict drive access to a set of directories (e.g. home folder or workspace root). When the list is non-empty, requests whose resolved path does not start with one of the listed prefixes should be rejected with `403`.
|
|
923
|
+
>
|
|
924
|
+
> When implementing it, reuse `isWithinAllowedPrefix()` from `services/workspace-root-path-policy.mjs` rather than writing a second `startsWith` check — a bare prefix match would let `C:\work` admit `C:\work-secrets`. See `workspaceRootAllowList` below, which already follows this pattern.
|
|
925
|
+
|
|
926
|
+
### Git changes modal
|
|
927
|
+
|
|
928
|
+
The **🌿 Git changes** entry in the conversation `⋯` menu opens a modal over the conversation's
|
|
929
|
+
workspace root, backed by `services/git-changes-service.mjs` and `routes/git-routes.mjs`:
|
|
930
|
+
|
|
931
|
+
- `GET /api/git/status?conversationId=…` — branch, upstream, ahead/behind, and every changed file
|
|
932
|
+
(staged, unstaged, and untracked; porcelain v1 parsing). The root is resolved server-side from the
|
|
933
|
+
conversation scope exactly like the file-preview routes — the client never sends a path — and a
|
|
934
|
+
workspace root that is not a git repository returns `isRepo: false` rather than an error.
|
|
935
|
+
- `GET /api/git/diff?path=…&untracked=0|1&conversationId=…` — one full-context unified diff
|
|
936
|
+
(`-U999999`, untracked files via `git diff --no-index /dev/null`). The client renders both the
|
|
937
|
+
**Changes only** view (context collapsed to gap markers) and the **Full file** view from this
|
|
938
|
+
single patch; the parsing lives in `public/app/git-diff-model.mjs`.
|
|
939
|
+
- `POST /api/git/pull?conversationId=…` — runs `git pull` in the workspace root and returns the
|
|
940
|
+
combined output.
|
|
941
|
+
|
|
942
|
+
File paths passed to `/api/git/diff` go through the same `resolveWorkspaceFilePath` containment
|
|
943
|
+
check as the file bridge, so traversal outside the workspace root is rejected.
|
|
944
|
+
|
|
945
|
+
### Changing the launch CWD
|
|
946
|
+
|
|
947
|
+
Two endpoints set a conversation's launch directory. Both accept the same aliases —
|
|
948
|
+
`rootPath`, `workspaceRootPath`, `workspace_root_path`, `cwd` — and both validate the
|
|
949
|
+
path before touching the database.
|
|
950
|
+
|
|
951
|
+
| Endpoint | Effect |
|
|
952
|
+
| --- | --- |
|
|
953
|
+
| `POST /api/conversation/:id/workspace-root` | Saves the next-launch CWD. Does not touch the running CLI. |
|
|
954
|
+
| `POST /api/conversation/:id/relaunch-with-workspace-root` | Saves the CWD, stops the CLI, and relaunches it in the new directory. |
|
|
955
|
+
|
|
956
|
+
**Validation** (`services/workspace-root-path-policy.mjs`): the path must be absolute
|
|
957
|
+
(drive-relative `C:foo` and extended-length `\\?\` forms are rejected), must not contain
|
|
958
|
+
`; & | \0 \r \n` — the same characters the chat `cd` command already rejects — and must
|
|
959
|
+
resolve, via `realpath`, to an existing directory. The resolved real path is what gets
|
|
960
|
+
persisted and spawned, so a symlink cannot smuggle a request past the allow list.
|
|
961
|
+
Failures return `400` with a `code` of `missing-root-path`, `invalid-root-path`,
|
|
962
|
+
`relative-root-path`, or `root-path-not-found`.
|
|
963
|
+
|
|
964
|
+
**`workspaceRootAllowList`** (optional, `server/config.json`, or the
|
|
965
|
+
`COPILOT_WORKSPACE_ROOT_ALLOW_LIST` env var using the platform path delimiter): an array of
|
|
966
|
+
absolute path prefixes. **Absent, `null`, `[]`, or not an array disables the check entirely**,
|
|
967
|
+
which is the default and matches the historical "any existing directory" behaviour. When it is
|
|
968
|
+
non-empty, a request outside every prefix is rejected with `403` and
|
|
969
|
+
`code: "root-path-not-allowed"`. Matching is on segment boundaries and is case-insensitive on
|
|
970
|
+
Windows. Entries that do not resolve to a directory are logged and dropped rather than failing
|
|
971
|
+
closed, so a typo cannot brick the relay.
|
|
972
|
+
|
|
973
|
+
**Concurrency.** All three endpoints that reach the session-worker launch path — the relaunch
|
|
974
|
+
route, the save route, and `POST /api/session-worker/:sdkSessionId/launch` — share one mutex
|
|
975
|
+
keyed by `sdkSessionId`. On top of that, the relaunch route coalesces duplicate requests
|
|
976
|
+
(a mobile double-tap, a second tab, a retried fetch) within a 5 s window:
|
|
977
|
+
|
|
978
|
+
- same request in flight → awaits it and returns the same body plus `coalesced: true`
|
|
979
|
+
- same request just settled → replays the cached body with `coalesced: true, cached: true`
|
|
980
|
+
- a *different* request in flight → `409` with `code: "relaunch_in_progress"`
|
|
981
|
+
|
|
982
|
+
Request identity is `conversationId` + the normalized path, or an explicit `Idempotency-Key`
|
|
983
|
+
header / `idempotencyKey` body field, which the web UI mints once per user gesture.
|
|
984
|
+
Both endpoints are additionally rate limited to 6 requests per 10 s per session and client IP,
|
|
985
|
+
returning `429` with `Retry-After`.
|
|
986
|
+
|
|
987
|
+
**Stop semantics.** The relaunch stops every process in the session's tree, waits for them to
|
|
988
|
+
exit, and escalates once (`SIGKILL` on POSIX; a re-enumerated forced pass on Windows, which
|
|
989
|
+
catches children spawned after the first snapshot). If anything survives, the relay **does not
|
|
990
|
+
launch** — it returns `409` with `code: "worker-stop-timeout"` and `remainingPids`, having still
|
|
991
|
+
saved the CWD for the next clean launch. Launching on top of a survivor is what previously left
|
|
992
|
+
two CLIs claiming one session.
|
|
993
|
+
|
|
994
|
+
**Response fields.** A successful relaunch reports what actually happened rather than a bare
|
|
995
|
+
`ok: true`:
|
|
996
|
+
|
|
997
|
+
| Field | Meaning |
|
|
998
|
+
| --- | --- |
|
|
999
|
+
| `relaunched` | A fresh process was started in the requested directory. |
|
|
1000
|
+
| `workspaceRootApplied` | The running CLI is actually in the requested directory. |
|
|
1001
|
+
| `reusedExistingProcess` | An existing process was reused, so the new CWD did **not** take effect. |
|
|
1002
|
+
| `warning: "cwd-not-applied"` | Present with `activeWorkspaceRootPath` and a human-readable `message` whenever `workspaceRootApplied` is `false`. |
|
|
1003
|
+
| `stoppedPids` / `launchedPid` | Which processes were killed and which was started. |
|
|
1004
|
+
| `coalesced` / `cached` | This response was replayed from a duplicate request. |
|
|
1005
|
+
|
|
1006
|
+
**Recent CWD history.** `recent_workspace_roots` is keyed by `path_key`, a case-normalized form
|
|
1007
|
+
of the path (lower-cased in full on Windows, untouched on POSIX), so `C:\Git\Repo` and
|
|
1008
|
+
`c:\git\repo` occupy one row and the 12-entry cap counts distinct directories. The runtime
|
|
1009
|
+
rebuilds an older table in place at startup; `server/migrations/0002-recent-workspace-roots-path-key.mjs`
|
|
1010
|
+
performs the same rebuild offline.
|
|
1011
|
+
- The web UI opens workspace mentions in an in-app preview dialog with **Preview / Raw** mode buttons.
|
|
1012
|
+
- Markdown preview supports optional embedded-HTML mode with script/event-handler stripping and a visible warning.
|
|
1013
|
+
- The floating **📁 Browse files** button opens the explorer with **Workspace**, **Drives**, and (when available) **Session** roots, tree navigation, list/icon folder views, and image thumbnails.
|
|
1014
|
+
|
|
1015
|
+
### Browser filter persistence and refresh behavior
|
|
1016
|
+
|
|
1017
|
+
- The **Hidden** and **Heavy** toolbar toggles are persisted in `localStorage`, under separate keys
|
|
1018
|
+
for the workspace root and the drives/session roots (the toolbar labels them differently:
|
|
1019
|
+
"Hidden" vs "Hidden/System"). Every filter defaults to off, so a fresh browser profile behaves
|
|
1020
|
+
exactly as it did before persistence existed.
|
|
1021
|
+
- Refreshing the browser — via the Refresh button or a filter toggle — refetches the lazy tree and
|
|
1022
|
+
then re-opens the folders that were expanded, rather than collapsing back to the root. A rapid
|
|
1023
|
+
double-toggle is safe: the restore is applied against the tree that actually arrived last.
|
|
1024
|
+
- Switching root, or a session root changing underneath the browser, discards a parked restore
|
|
1025
|
+
because the saved path belongs to the tree being left.
|
|
1026
|
+
|
|
1027
|
+
### External link handling
|
|
1028
|
+
|
|
1029
|
+
Links in chat bubbles, question cards, and subagent bubbles are opened in a new tab with
|
|
1030
|
+
`noopener`/`noreferrer` applied, including links added to the DOM after page load. Workspace file
|
|
1031
|
+
links (`@file:` mentions and file-preview markdown) are exempt so they continue to open the in-app
|
|
1032
|
+
preview.
|
|
1033
|
+
|
|
1034
|
+
## Structured Answers and Multi-Field Elicitation
|
|
1035
|
+
|
|
1036
|
+
The relay supports **structured answer forms** that extend beyond simple text `ask_user` questions. When a Copilot agent or tool needs multi-field input (e.g., confirmation with multiple checkboxes, form data with validation), the relay bridges the question into a web form.
|
|
1037
|
+
|
|
1038
|
+
### Database Schema
|
|
1039
|
+
|
|
1040
|
+
Structured answers are stored in the `relay_questions` table:
|
|
1041
|
+
|
|
1042
|
+
- `id` (primary key, UUID)
|
|
1043
|
+
- `request_schema` (JSON schema defining form fields: types, validation, constraints)
|
|
1044
|
+
- `structured_answer` (JSON object containing user's multi-field response)
|
|
1045
|
+
|
|
1046
|
+
The migration `server/migrations/0001-add-structured-answer.mjs` creates the `structured_answer` and `request_schema` columns.
|
|
1047
|
+
|
|
1048
|
+
### Elicitation Flow
|
|
1049
|
+
|
|
1050
|
+
1. **Agent sends multi-field question** → Copilot SDK sends `ElicitationRequest` with `requestSchema` (JSON schema).
|
|
1051
|
+
2. **Extension bridges question** → `.github/extensions/web-relay/skills/question-routing-hooks.mjs` detects structured schema and registers it in the relay.
|
|
1052
|
+
3. **Browser renders form** → `server/public/app/question-schema-view.mjs` generates UI from the JSON schema (text inputs, checkboxes, dropdowns, etc.).
|
|
1053
|
+
4. **User submits form** → `ask-user-view.js` validates and POSTs to `/api/ask-user` with `structuredAnswer` payload.
|
|
1054
|
+
5. **Relay stores + returns** → Answer stored in `structured_answer` column; extension retrieves via `/api/pending` and returns `ElicitationResponse` to SDK.
|
|
1055
|
+
|
|
1056
|
+
### Schema Example
|
|
1057
|
+
|
|
1058
|
+
```json
|
|
1059
|
+
{
|
|
1060
|
+
"type": "object",
|
|
1061
|
+
"properties": {
|
|
1062
|
+
"confirmation": {
|
|
1063
|
+
"type": "boolean",
|
|
1064
|
+
"title": "Do you want to proceed?"
|
|
1065
|
+
},
|
|
1066
|
+
"environment": {
|
|
1067
|
+
"type": "string",
|
|
1068
|
+
"enum": ["dev", "staging", "prod"],
|
|
1069
|
+
"title": "Select environment"
|
|
1070
|
+
},
|
|
1071
|
+
"tags": {
|
|
1072
|
+
"type": "array",
|
|
1073
|
+
"items": { "type": "string" },
|
|
1074
|
+
"title": "Enter tags"
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
"required": ["confirmation", "environment"]
|
|
1078
|
+
}
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
### Browser Behavior
|
|
1082
|
+
|
|
1083
|
+
- Multi-field forms render form controls for each schema property.
|
|
1084
|
+
- Validation happens client-side (required fields, type checking) and server-side.
|
|
1085
|
+
- Single-field simple `ask_user` (legacy) still renders as a single text input or button group.
|
|
1086
|
+
|
|
1087
|
+
## Relay Tool Guidance
|
|
1088
|
+
|
|
1089
|
+
The CLI extension (`.github/extensions/web-relay/extension.mjs`) loads `relay-tools.md`
|
|
1090
|
+
for shared tool guidance.
|
|
1091
|
+
|
|
1092
|
+
The browser UI keeps the usage button in the sidebar header, and that button continues to
|
|
1093
|
+
call `/api/usage` directly. It now renders the unified plan-usage report — one card per
|
|
1094
|
+
provider with meters, reset countdowns and collapsible cost detail — via
|
|
1095
|
+
`public/app/plan-usage-view.mjs`, falling back to the old Copilot-only text summary when
|
|
1096
|
+
the response carries no `providers` array (an older relay, or one without the plan-usage
|
|
1097
|
+
service).
|
|
1098
|
+
|
|
1099
|
+
## Config (`config.json`)
|
|
1100
|
+
|
|
1101
|
+
```json
|
|
1102
|
+
{
|
|
1103
|
+
"authToken": "<your-token>",
|
|
1104
|
+
"port": 3333,
|
|
1105
|
+
"localhostOnly": true,
|
|
1106
|
+
"pollIntervalMs": 3000,
|
|
1107
|
+
"processingTimeoutMs": 600000,
|
|
1108
|
+
"conversationSessionMode": "isolated",
|
|
1109
|
+
"contextIndicatorMode": "default",
|
|
1110
|
+
"sdkVersion": "1.0.63",
|
|
1111
|
+
"sdkPath": "/absolute/path/to/copilot-sdk/index.js",
|
|
1112
|
+
"cliPath": "/absolute/path/to/app.js",
|
|
1113
|
+
"sshTunnel": {
|
|
1114
|
+
"mode": "disabled",
|
|
1115
|
+
"required": false,
|
|
1116
|
+
"remoteBind": "loopback",
|
|
1117
|
+
"command": "ssh",
|
|
1118
|
+
"user": "ubuntu",
|
|
1119
|
+
"host": "relay.example.com",
|
|
1120
|
+
"remotePort": 4444,
|
|
1121
|
+
"identityFile": "~/.ssh/id_rsa",
|
|
1122
|
+
"autoReclaimPort": true
|
|
1123
|
+
},
|
|
1124
|
+
"cloudflaredTunnel": {
|
|
1125
|
+
"mode": "disabled",
|
|
1126
|
+
"required": false,
|
|
1127
|
+
"token": "",
|
|
1128
|
+
"binary": "",
|
|
1129
|
+
"extraArgs": []
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
| Key | Default | Description |
|
|
1135
|
+
|---|---|---|
|
|
1136
|
+
| `authToken` | *(required)* | Token for all API / UI access |
|
|
1137
|
+
| `port` | `3333` | HTTP/WebSocket listen port |
|
|
1138
|
+
| `localhostOnly` | `true` | Bind only to loopback (`127.0.0.1`) and block LAN/WAN access |
|
|
1139
|
+
| `pollIntervalMs` | `3000` | CLI poll interval (ms) |
|
|
1140
|
+
| `processingTimeoutMs` | `600000` | Worker-inactivity window before a turn is treated as stale (ms) — see [Turn recovery](#turn-recovery-and-the-max-turn-duration) |
|
|
1141
|
+
| `conversationSessionMode` | `isolated` | SDK session strategy (`isolated` or `shared`) |
|
|
1142
|
+
| `contextIndicatorMode` | `default` | Input context indicator style (`default` shimmer line or `bar` fill indicator) |
|
|
1143
|
+
| `sdkVersion` | *(latest detected)* | Optional semver pin (for example `1.0.63`) used by relay SDK auto-detection |
|
|
1144
|
+
| `sdkPath` | *(auto-detected)* | Optional absolute override for Copilot SDK entry path (`.../copilot-sdk/index.js`) |
|
|
1145
|
+
| `cliPath` | *(auto-detected)* | Optional absolute override for Copilot CLI app path (`.../app.js`) |
|
|
1146
|
+
| `restartGracefulTimeoutMs` | `8000` | Graceful shutdown wait before force fallback |
|
|
1147
|
+
| `restartShutdownTimeoutMs` | `45000` | Drain timeout while waiting for active queue jobs |
|
|
1148
|
+
| `restartSpawnTimeoutMs` | `18000` | Max wait for resume process to leave online state |
|
|
1149
|
+
| `restartRebindTimeoutMs` | `20000` | Max wait for session-sync rebind confirmation |
|
|
1150
|
+
| `restartMaxAttempts` | `3` | Max restart attempts before terminal exhaustion |
|
|
1151
|
+
| `restartRetryBackoffMs` | `[1000,3000,7000]` | Deterministic retry backoff schedule |
|
|
1152
|
+
| `sshTunnel.mode` | `disabled` | Tunnel mode (`disabled` or `managed`) |
|
|
1153
|
+
| `sshTunnel.enabled` | `false` | Legacy alias for mode (`true` => `managed`, `false` => `disabled`) |
|
|
1154
|
+
| `sshTunnel.required` | `false` | Pause dequeue when tunnel is disconnected in managed mode |
|
|
1155
|
+
| `sshTunnel.remoteBind` | `loopback` | Remote bind mode for SSH `-R` (`loopback` or `public`) |
|
|
1156
|
+
| `sshTunnel.command` | `ssh` | SSH executable path or command name |
|
|
1157
|
+
| `sshTunnel.user` | — | SSH user on VPS |
|
|
1158
|
+
| `sshTunnel.host` | — | VPS hostname / IP |
|
|
1159
|
+
| `sshTunnel.remotePort` | — | Port opened on the VPS |
|
|
1160
|
+
| `sshTunnel.identityFile` | *(optional)* | SSH private key path (`~` expanded); uses ssh-agent if omitted |
|
|
1161
|
+
| `sshTunnel.autoReclaimPort` | `true` | When remote bind fails, run a remote reclaim step before retrying |
|
|
1162
|
+
| `sshTunnel.reclaimStaleSshSessions` | `false` | Also kill your own childless `@notty` SSH sessions when the port stays held (see caveat below) |
|
|
1163
|
+
| `sshTunnel.remoteCleanupCommand` | *(optional)* | Override reclaim command (`ssh user@host <command>`) for custom VPS cleanup |
|
|
1164
|
+
| `cloudflaredTunnel.mode` | `disabled` | Cloudflare tunnel mode (`disabled` or `managed`) |
|
|
1165
|
+
| `cloudflaredTunnel.enabled` | `false` | Legacy alias for mode (`true` => `managed`) |
|
|
1166
|
+
| `cloudflaredTunnel.required` | `false` | Pause dequeue when the Cloudflare tunnel is disconnected in managed mode |
|
|
1167
|
+
| `cloudflaredTunnel.token` | — | Tunnel token from the router panel (never logged) |
|
|
1168
|
+
| `cloudflaredTunnel.binary` | *(auto)* | `cloudflared` path; falls back to the optional npm package, then `PATH` |
|
|
1169
|
+
| `cloudflaredTunnel.extraArgs` | `[]` | Extra arguments appended to `cloudflared tunnel run` |
|
|
1170
|
+
| `tunnelMarkerHeaders` | `[]` | Extra edge-injected header names that mark tunnel traffic for the session-worker path guard |
|
|
1171
|
+
|
|
1172
|
+
### SDK auto-detection behavior
|
|
1173
|
+
|
|
1174
|
+
- Relay first honors explicit `sdkPath` + `cliPath` from `config.json` when both are set.
|
|
1175
|
+
- Otherwise it auto-detects the highest available semver under platform-specific install roots:
|
|
1176
|
+
- **Windows:** `%LOCALAPPDATA%\copilot\pkg`
|
|
1177
|
+
- **Linux:** `$XDG_CACHE_HOME/copilot/pkg` (default `~/.cache/copilot/pkg`), then `$XDG_DATA_HOME/copilot/pkg` (default `~/.local/share/copilot/pkg`)
|
|
1178
|
+
- **macOS:** `~/Library/Application Support/copilot/pkg`
|
|
1179
|
+
- Within each root it checks platform subdirs (`<platform>-<arch>`, then `universal`) and picks the highest version containing both:
|
|
1180
|
+
- `copilot-sdk/index.js`
|
|
1181
|
+
- `app.js`
|
|
1182
|
+
- Optional environment override: set `COPILOT_PKG_DIR` to force a specific pkg base directory.
|
|
1183
|
+
- Optional config pin: set `sdkVersion` to require an exact version during auto-detection.
|
|
1184
|
+
|
|
1185
|
+
## SSH Reverse Tunnel
|
|
1186
|
+
|
|
1187
|
+
When `sshTunnel.mode` is `managed` (or legacy `sshTunnel.enabled=true`), `server.js` spawns:
|
|
1188
|
+
|
|
1189
|
+
```
|
|
1190
|
+
ssh -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
|
|
1191
|
+
-o StrictHostKeyChecking=accept-new -o BatchMode=yes \
|
|
1192
|
+
-o ExitOnForwardFailure=yes \
|
|
1193
|
+
-R <remoteSpec> <user>@<host>
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
`sshTunnel.remoteBind` controls `<remoteSpec>`:
|
|
1197
|
+
- `loopback` => `<remotePort>:127.0.0.1:<port>` (loopback bind, best with Caddy `reverse_proxy localhost:<remotePort>`)
|
|
1198
|
+
- `public` => `*:<remotePort>:127.0.0.1:<port>`
|
|
1199
|
+
`ExitOnForwardFailure=yes` ensures the tunnel process exits immediately if remote port forwarding fails, allowing clean auto-retry instead of a false "connected" state.
|
|
1200
|
+
|
|
1201
|
+
**Auto-reconnect** — if the tunnel exits for any reason it is rescheduled with
|
|
1202
|
+
exponential backoff (5 s → 10 s → 20 s → 40 s → 60 s cap, no retry limit).
|
|
1203
|
+
The counter resets after a connection is stable for >30 s.
|
|
1204
|
+
|
|
1205
|
+
**Remote stale-forward reclaim** — on `remote port forwarding failed for listen port`,
|
|
1206
|
+
the relay runs a one-shot remote cleanup over SSH and retries quickly on the same
|
|
1207
|
+
fixed port. Set `sshTunnel.autoReclaimPort` to `false` to disable, or provide
|
|
1208
|
+
`sshTunnel.remoteCleanupCommand` for your own server-specific cleanup command.
|
|
1209
|
+
|
|
1210
|
+
The reclaim reads the port state from `ss` / `/proc/net/tcp` rather than trusting
|
|
1211
|
+
`lsof`. This matters: when the port is held by a stale `ssh -R` forward, the owning
|
|
1212
|
+
`sshd` session has dropped privileges, so `/proc/<pid>/fd` is root-owned and both
|
|
1213
|
+
`lsof` and `fuser` report nothing even though the socket belongs to your own uid.
|
|
1214
|
+
The cleanup exits non-zero while the port is still bound, so a port that cannot be
|
|
1215
|
+
freed falls back to exponential backoff instead of respawning `ssh` every second.
|
|
1216
|
+
|
|
1217
|
+
Killing such a forward requires killing the `sshd` session that owns it. That is
|
|
1218
|
+
off by default because the session cannot be identified precisely without root.
|
|
1219
|
+
Enabling `sshTunnel.reclaimStaleSshSessions` sweeps your own `sshd` sessions that
|
|
1220
|
+
have no child processes — the shape `ssh -N -R` leaves behind. Interactive shells,
|
|
1221
|
+
IDE remote servers (VS Code / Cursor) and the reclaim command itself all have
|
|
1222
|
+
children and are skipped, but any *other* `-N` forward you rely on for a different
|
|
1223
|
+
port would also be killed, so leave it off unless the relay owns the account.
|
|
1224
|
+
|
|
1225
|
+
**Caddy VPS config:**
|
|
1226
|
+
|
|
1227
|
+
```
|
|
1228
|
+
relay.example.com {
|
|
1229
|
+
reverse_proxy localhost:4444
|
|
1230
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
**`/api/status`** now includes:
|
|
1234
|
+
|
|
1235
|
+
```json
|
|
1236
|
+
"contextIndicatorMode": "default",
|
|
1237
|
+
"sshTunnel": {
|
|
1238
|
+
"mode": "managed",
|
|
1239
|
+
"required": false,
|
|
1240
|
+
"enabled": true,
|
|
1241
|
+
"blocking": false,
|
|
1242
|
+
"connected": true,
|
|
1243
|
+
"host": "relay.example.com",
|
|
1244
|
+
"remotePort": 4444,
|
|
1245
|
+
"remoteBindMode": "loopback",
|
|
1246
|
+
"reconnectAttempts": 0,
|
|
1247
|
+
"connectedSince": "2026-05-18T01:00:00.000Z",
|
|
1248
|
+
"lastError": null,
|
|
1249
|
+
"valid": true
|
|
1250
|
+
}
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
### Managed vs. Disabled Mode
|
|
1254
|
+
|
|
1255
|
+
| Setting | Behavior |
|
|
1256
|
+
|---------|----------|
|
|
1257
|
+
| `mode: "disabled"` | No SSH tunnel; relay is only accessible locally (localhost). Suitable for development or internal network access. |
|
|
1258
|
+
| `mode: "managed"` | SSH tunnel enabled; relay opens reverse port forward to VPS. Suitable for remote access or internet-facing deployments. |
|
|
1259
|
+
|
|
1260
|
+
### Tunnel Connection & Queueing
|
|
1261
|
+
|
|
1262
|
+
- **Tunnel required = false** (default): Queue continues to process even if tunnel is disconnected. Browser can still interact via localhost.
|
|
1263
|
+
- **Tunnel required = true**: Queue is paused (returns `paused: true` with reason `ssh_tunnel_required`) when tunnel is disconnected. Used when relay **must** be accessible via VPS (e.g., mobile browser always connects through VPS).
|
|
1264
|
+
|
|
1265
|
+
When `required=true` and tunnel is down:
|
|
1266
|
+
- Relay stays running internally
|
|
1267
|
+
- `/api/pending` returns `{ paused: true, reason: "ssh_tunnel_required" }`
|
|
1268
|
+
- Extension stops dequeuing turns
|
|
1269
|
+
- Browser sees "waiting for tunnel to reconnect..." status
|
|
1270
|
+
- Once tunnel reconnects, queue resumes automatically
|
|
1271
|
+
|
|
1272
|
+
### SSH Tunnel Configuration Precedence
|
|
1273
|
+
|
|
1274
|
+
1. `server/config.json` → `sshTunnel` object
|
|
1275
|
+
2. Environment variables → `COPILOT_REMOTE_SSH_TUNNEL_*` prefixed keys
|
|
1276
|
+
|
|
1277
|
+
Example environment overrides:
|
|
1278
|
+
```bash
|
|
1279
|
+
COPILOT_REMOTE_SSH_TUNNEL_MODE=managed \
|
|
1280
|
+
COPILOT_REMOTE_SSH_TUNNEL_USER=deployer \
|
|
1281
|
+
COPILOT_REMOTE_SSH_TUNNEL_HOST=vps.example.com \
|
|
1282
|
+
COPILOT_REMOTE_SSH_TUNNEL_REMOTE_PORT=4444 \
|
|
1283
|
+
npm start
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
### SSH tunnel migration notes
|
|
1287
|
+
|
|
1288
|
+
- `sshTunnel.mode` is now the canonical switch. Use `disabled` for direct relay deployments and `managed` for reverse SSH.
|
|
1289
|
+
- `sshTunnel.enabled` still works for compatibility and maps to `mode`.
|
|
1290
|
+
- Add `sshTunnel.required: true` only when queue dequeue must stop until the tunnel reconnects.
|
|
1291
|
+
|
|
1292
|
+
`restartOrchestrator` in `/api/status` and `/api/restart-orchestrator` now exposes
|
|
1293
|
+
attempt/retry/timeout fields (`attempts`, `maxAttempts`, `retryAt`, `retryBackoffMs`,
|
|
1294
|
+
`spawnDeadlineAt`, `rebindDeadlineAt`) plus terminal outcomes
|
|
1295
|
+
(`terminalOutcomeCode`, `terminalOutcomeMessage`, `terminalOutcomeAttempts`).
|
|
1296
|
+
Failure classes are deterministic: transient timeouts (`spawn-timeout`, `rebind-timeout`)
|
|
1297
|
+
retry with bounded backoff; session mismatch conflicts (`transaction-mismatch`, `target-mismatch`)
|
|
1298
|
+
are terminal and stop retrying.
|
|
1299
|
+
`POST /api/session-sync` accepts optional orchestrator correlation/target/rebind fields:
|
|
1300
|
+
`orchestrator_correlation_id`, `orchestrator_target_session_id`, and `rebind_completed`
|
|
1301
|
+
(or `rebind_state=completed`). Rebind mismatches return `409` with `retryable`/`terminal`.
|
|
1302
|
+
The extension dequeue/send path treats this restart-orchestrator flow as authoritative and
|
|
1303
|
+
does not attempt in-process runtime session switch calls.
|
|
1304
|
+
|
|
1305
|
+
This worker restart-orchestrator flow is separate from the manual relay self-restart state
|
|
1306
|
+
reported as `relayShutdown`.
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
## Database Migrations
|
|
1311
|
+
|
|
1312
|
+
The relay uses a simple SQL migration system to evolve the SQLite schema over time.
|
|
1313
|
+
|
|
1314
|
+
**Migration File Location:** `server/migrations/`
|
|
1315
|
+
|
|
1316
|
+
**Migration Lifecycle:**
|
|
1317
|
+
|
|
1318
|
+
1. On server startup, relay checks the database for applied migrations
|
|
1319
|
+
2. Unapplied migrations (in filename order) are executed automatically
|
|
1320
|
+
3. Each migration file exports a `up()` function that creates/alters tables
|
|
1321
|
+
4. After successful execution, migration is recorded in the database metadata
|
|
1322
|
+
5. Migrations are idempotent: re-running a migration is safe (no duplicates)
|
|
1323
|
+
|
|
1324
|
+
**Currently Deployed Migration:**
|
|
1325
|
+
|
|
1326
|
+
- **`0001-add-structured-answer.mjs`** — Adds `structured_answer` and `request_schema` columns to the `relay_questions` table to support multi-field elicitation forms (see [Structured Answers and Multi-Field Elicitation](#structured-answers-and-multi-field-elicitation) section).
|
|
1327
|
+
- `request_schema` (TEXT/JSON) — JSON schema defining form fields and validation
|
|
1328
|
+
- `structured_answer` (TEXT/JSON) — User's submitted multi-field response object
|
|
1329
|
+
|
|
1330
|
+
**In-place column additions:**
|
|
1331
|
+
|
|
1332
|
+
Some columns are added directly at startup with guarded `ALTER TABLE` statements rather than
|
|
1333
|
+
migration files. Recent additions on `runtime_sessions`:
|
|
1334
|
+
|
|
1335
|
+
- `claude_native_session_id` (TEXT) — native Claude Agent SDK session id, replayed as `resume`
|
|
1336
|
+
- `context_usage_json` (TEXT/JSON) — latest context-window breakdown reported by a Claude worker
|
|
1337
|
+
- `context_usage_captured_at` (TEXT) — when that breakdown was captured
|
|
1338
|
+
|
|
1339
|
+
and on `messages`:
|
|
1340
|
+
|
|
1341
|
+
- `hidden_from_shares` / `share_hidden_at` — per-message share visibility (see
|
|
1342
|
+
[Conversation sharing](#conversation-sharing-and-per-message-visibility))
|
|
1343
|
+
|
|
1344
|
+
New tables are likewise created at startup with `CREATE TABLE IF NOT EXISTS` in both the fresh-schema
|
|
1345
|
+
and migration blocks — most recently `workflow_runs` (final workflow digest per assistant message,
|
|
1346
|
+
keyed on `response_message_id`, cascade-deleted with the conversation; see
|
|
1347
|
+
[Background tasks and workflow progress](#background-tasks-and-workflow-progress)).
|
|
1348
|
+
|
|
1349
|
+
Statements that depend on these columns are prepared conditionally, so an older database keeps
|
|
1350
|
+
working with the corresponding feature inert rather than crashing at startup.
|
|
1351
|
+
|
|
1352
|
+
**To Deploy New Migrations:**
|
|
1353
|
+
|
|
1354
|
+
1. Create file `server/migrations/000N-description.mjs` (increment the number)
|
|
1355
|
+
2. Export an async `up()` function that receives the database connection
|
|
1356
|
+
3. Write SQL alterations:
|
|
1357
|
+
```javascript
|
|
1358
|
+
export async function up(db) {
|
|
1359
|
+
await db.exec(`ALTER TABLE some_table ADD COLUMN new_col TEXT;`);
|
|
1360
|
+
}
|
|
1361
|
+
```
|
|
1362
|
+
4. Restart the relay; migration runs automatically
|
|
1363
|
+
5. Verify: check relay logs for `[migrations] Applying 000N-description`
|
|
1364
|
+
|
|
1365
|
+
## Files
|
|
1366
|
+
|
|
1367
|
+
| File | Description |
|
|
1368
|
+
|------|-------------|
|
|
1369
|
+
| `server.js` | Main server |
|
|
1370
|
+
| `public/index.html` | Web chat UI |
|
|
1371
|
+
| `config.json` | Auth token and settings (gitignored) |
|
|
1372
|
+
| `data/copilot.db` | Persisted conversations, settings, and queue storage (gitignored) |
|
|
1373
|
+
| `relay-tools.md` | Markdown tool guidance loaded by the relay extension |
|
|
1374
|
+
| `claude-worker/` | Claude Agent SDK session worker (turn runner, ask-user bridge, attachments, SDK message normalizer) |
|
|
1375
|
+
| `services/claude-session-root-service.mjs` | Resolves the browsable session folder for Claude conversations |
|
|
1376
|
+
| `services/context-usage-view.mjs` | Normalizes Copilot and Claude context data into one payload |
|
|
1377
|
+
| `services/cloudflared-tunnel-service.mjs` | Supervises the `cloudflared` child process for the Cloudflare Tunnel mode |
|
|
1378
|
+
| `services/tunnel-worker-path-guard.mjs` | Rejects session-worker WebSocket paths that arrive through a public tunnel |
|
|
1379
|
+
| `../shared/turn-ceiling.mjs` | Shared bounds/formatting for the max turn duration setting |
|