@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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { createShareVisibilityStatements } from './share-visibility-statements.mjs';
|
|
4
|
+
|
|
5
|
+
export function createSessionRepository(db) {
|
|
6
|
+
const shareVisibility = createShareVisibilityStatements(db);
|
|
7
|
+
const runtimeSessionColumns = new Set(
|
|
8
|
+
db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((column) => String(column?.name || '').trim()),
|
|
9
|
+
);
|
|
10
|
+
const runtimeSessionsSupportProviders = runtimeSessionColumns.has('provider_type')
|
|
11
|
+
&& runtimeSessionColumns.has('provider_model');
|
|
12
|
+
const runtimeProviderSelect = runtimeSessionsSupportProviders
|
|
13
|
+
? 'rs.model AS runtime_model, rs.provider_type AS runtime_provider_type, rs.provider_model AS runtime_provider_model'
|
|
14
|
+
: 'rs.model AS runtime_model, NULL AS runtime_provider_type, NULL AS runtime_provider_model';
|
|
15
|
+
return {
|
|
16
|
+
// conversations
|
|
17
|
+
getConv: db.prepare(`SELECT * FROM conversations WHERE id = ? AND status != 'deleted'`),
|
|
18
|
+
getConvAnyStatus: db.prepare(`SELECT * FROM conversations WHERE id = ?`),
|
|
19
|
+
getConvBySdkSessionId: db.prepare(`SELECT * FROM conversations WHERE sdk_session_id = ? AND status != 'deleted' ORDER BY updated_at DESC LIMIT 1`),
|
|
20
|
+
listConvIdsMissingRuntimeSession: db.prepare(`SELECT c.id AS id FROM conversations c LEFT JOIN runtime_sessions rs ON rs.conversation_id = c.id WHERE rs.id IS NULL AND c.status != 'deleted'`),
|
|
21
|
+
runtimeSessionsSupportProviders,
|
|
22
|
+
listConvs: db.prepare(`SELECT c.id, c.title, c.title_source, c.archived, c.compacted_into, c.compacted_from, c.sdk_session_id, c.preferred_relay_mode, c.preferred_model, c.preferred_reasoning_effort, c.configured_workspace_root_path, c.runtime_workspace_root_path, c.draft_text, c.draft_updated_at, c.draft_updated_by_client_id, c.draft_attachments, c.created_at, c.updated_at, rs.id AS runtime_session_id, rs.strategy AS runtime_strategy, rs.status AS runtime_status, rs.last_used_at AS runtime_last_used_at, ${runtimeProviderSelect}, COUNT(m.id) as message_count FROM conversations c LEFT JOIN messages m ON m.conversation_id = c.id LEFT JOIN runtime_sessions rs ON rs.conversation_id = c.id WHERE c.status != 'deleted' AND (? = 1 OR c.archived = 0) GROUP BY c.id ORDER BY CASE WHEN c.sdk_session_id IS NULL OR c.sdk_session_id = '' THEN 1 ELSE 0 END ASC, c.updated_at DESC`),
|
|
23
|
+
insertConv: db.prepare(`INSERT OR IGNORE INTO conversations (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)`),
|
|
24
|
+
updateConvTime: db.prepare(`UPDATE conversations SET updated_at = ? WHERE id = ?`),
|
|
25
|
+
updateConvTitle: db.prepare(`UPDATE conversations SET title = ?, title_source = 'manual' WHERE id = ?`),
|
|
26
|
+
updateConvPreferences: db.prepare(`UPDATE conversations SET preferred_relay_mode = ?, preferred_model = ?, preferred_reasoning_effort = ?, updated_at = ? WHERE id = ?`),
|
|
27
|
+
updateConvConfiguredWorkspaceRoot: db.prepare(`UPDATE conversations SET configured_workspace_root_path = ?, updated_at = ? WHERE id = ?`),
|
|
28
|
+
updateConvRuntimeWorkspaceRoot: db.prepare(`UPDATE conversations SET runtime_workspace_root_path = ?, updated_at = ? WHERE id = ?`),
|
|
29
|
+
seedConvConfiguredWorkspaceRootIfMissing: db.prepare(`UPDATE conversations SET configured_workspace_root_path = ?, updated_at = ? WHERE id = ? AND (configured_workspace_root_path IS NULL OR configured_workspace_root_path = '')`),
|
|
30
|
+
updateConvSeed: db.prepare(`UPDATE conversations SET summary_seed = ?, seed_pending = ?, compacted_from = ?, updated_at = ? WHERE id = ?`),
|
|
31
|
+
markConvCompacted: db.prepare(`UPDATE conversations SET archived = 1, compacted_into = ?, updated_at = ? WHERE id = ?`),
|
|
32
|
+
getConvSeed: db.prepare(`SELECT summary_seed, seed_pending FROM conversations WHERE id = ?`),
|
|
33
|
+
clearConvSeed: db.prepare(`UPDATE conversations SET seed_pending = 0, updated_at = ? WHERE id = ?`),
|
|
34
|
+
updateConvDraft: db.prepare(`UPDATE conversations SET draft_text = ?, draft_updated_at = ?, draft_updated_by_client_id = ? WHERE id = ?`),
|
|
35
|
+
updateConvDraftAttachments: db.prepare(`UPDATE conversations SET draft_attachments = ?, draft_updated_at = ?, draft_updated_by_client_id = ? WHERE id = ?`),
|
|
36
|
+
archiveConv: db.prepare(`UPDATE conversations SET archived = 1, updated_at = ? WHERE id = ?`),
|
|
37
|
+
deleteConv: db.prepare(`DELETE FROM conversations WHERE id = ?`),
|
|
38
|
+
|
|
39
|
+
// messages
|
|
40
|
+
getMessages: db.prepare(`SELECT * FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC`),
|
|
41
|
+
getSharedMessages: shareVisibility.getSharedMessages,
|
|
42
|
+
getMessageByConversation: db.prepare(`SELECT * FROM messages WHERE id = ? AND conversation_id = ? LIMIT 1`),
|
|
43
|
+
setMessageShareVisibility: shareVisibility.setMessageShareVisibility,
|
|
44
|
+
getConversationMessageCount: db.prepare(`SELECT COUNT(*) AS count FROM messages WHERE conversation_id = ?`),
|
|
45
|
+
getConversationActiveQueueCount: db.prepare(`SELECT COUNT(*) AS count FROM queue WHERE conversation_id = ? AND status IN ('pending', 'processing', 'parked')`),
|
|
46
|
+
getLatestConversationModel: db.prepare(`SELECT model FROM messages WHERE conversation_id = ? AND model IS NOT NULL AND model != '' ORDER BY timestamp DESC LIMIT 1`),
|
|
47
|
+
getRecentMessagesDesc: db.prepare(`SELECT role, text, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT ?`),
|
|
48
|
+
insertMsg: db.prepare(`INSERT INTO messages (id, conversation_id, role, text, model, mode, attachments, timestamp, model_requested, model_actual, model_origin) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
49
|
+
|
|
50
|
+
// queue
|
|
51
|
+
insertQ: db.prepare(`INSERT INTO queue (id, conversation_id, runtime_session_id, is_new_conversation, model, model_variant_id, reasoning_effort, relay_mode, text, attachments, status, timestamp, retry_count, next_attempt_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, 0, NULL)`),
|
|
52
|
+
findPending: db.prepare(`SELECT * FROM queue WHERE status = 'pending' AND (next_attempt_at IS NULL OR next_attempt_at <= ?) ORDER BY retry_count ASC, CASE WHEN next_attempt_at IS NULL THEN 0 ELSE 1 END ASC, COALESCE(next_attempt_at, timestamp) ASC, timestamp ASC LIMIT 1`),
|
|
53
|
+
countStatus: db.prepare(`SELECT status, COUNT(*) as cnt FROM queue WHERE status IN ('pending','processing','parked') GROUP BY status`),
|
|
54
|
+
countRuntimeSessions: db.prepare(`SELECT COUNT(*) AS cnt FROM runtime_sessions WHERE status = 'active'`),
|
|
55
|
+
setProcessing: db.prepare(`UPDATE queue SET status = 'processing', processing_at = ? WHERE id = ?`),
|
|
56
|
+
setQueueRuntimeSession: db.prepare(`UPDATE queue SET runtime_session_id = ? WHERE id = ?`),
|
|
57
|
+
setQueueResponseMessageId: db.prepare(`UPDATE queue SET response_message_id = ? WHERE id = ?`),
|
|
58
|
+
setDone: db.prepare(`UPDATE queue SET status = 'done', response = ?, processing_at = NULL, next_attempt_at = NULL WHERE id = ? AND status IN ('processing', 'pending')`),
|
|
59
|
+
setFailed: db.prepare(`UPDATE queue SET status = 'failed', response = ?, processing_at = NULL, next_attempt_at = NULL WHERE id = ?`),
|
|
60
|
+
deleteConvQ: db.prepare(`DELETE FROM queue WHERE conversation_id = ?`),
|
|
61
|
+
findQById: db.prepare(`SELECT * FROM queue WHERE id = ?`),
|
|
62
|
+
pruneQueue: db.prepare(`DELETE FROM queue WHERE status = 'done' AND id NOT IN (SELECT id FROM queue WHERE status = 'done' ORDER BY timestamp DESC LIMIT 200)`),
|
|
63
|
+
recoverStale: db.prepare(`UPDATE queue SET status = 'pending', processing_at = NULL, next_attempt_at = ? WHERE status = 'processing' AND processing_at < ?`),
|
|
64
|
+
listRecoverableProcessing: db.prepare(`SELECT id, conversation_id FROM queue WHERE status = 'processing' AND processing_at < ?`),
|
|
65
|
+
recoverProcessingBefore: db.prepare(`UPDATE queue SET status = 'pending', processing_at = NULL, next_attempt_at = ? WHERE status = 'processing' AND processing_at < ?`),
|
|
66
|
+
listQueueForPauseDrop: db.prepare(`SELECT id, conversation_id FROM queue WHERE status IN ('pending', 'processing', 'parked')`),
|
|
67
|
+
deleteQueueById: db.prepare(`DELETE FROM queue WHERE id = ?`),
|
|
68
|
+
getLatestProcessingQueueByConversation: db.prepare(`SELECT id, relay_mode, timestamp, processing_at FROM queue WHERE conversation_id = ? AND status = 'processing' ORDER BY COALESCE(processing_at, timestamp) DESC LIMIT 1`),
|
|
69
|
+
|
|
70
|
+
// runtime sessions
|
|
71
|
+
getRuntimeSessionByConversation: db.prepare(`SELECT * FROM runtime_sessions WHERE conversation_id = ?`),
|
|
72
|
+
getRuntimeSessionBySdkSessionId: db.prepare(`SELECT * FROM runtime_sessions WHERE sdk_session_id = ?`),
|
|
73
|
+
getRuntimeSessionById: db.prepare(`SELECT * FROM runtime_sessions WHERE id = ?`),
|
|
74
|
+
listRuntimeSessions: db.prepare(`SELECT rs.*, c.title AS conversation_title, c.updated_at AS conversation_updated_at FROM runtime_sessions rs LEFT JOIN conversations c ON c.id = rs.conversation_id ORDER BY rs.last_used_at DESC`),
|
|
75
|
+
insertRuntimeSession: db.prepare(runtimeSessionsSupportProviders
|
|
76
|
+
? `INSERT INTO runtime_sessions (id, conversation_id, strategy, runtime_key, model, status, created_at, last_used_at, sdk_session_id, provider_type, provider_model) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)`
|
|
77
|
+
: `INSERT INTO runtime_sessions (id, conversation_id, strategy, runtime_key, model, status, created_at, last_used_at, sdk_session_id) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?)`),
|
|
78
|
+
setConvSdkSessionIdIfMissing: db.prepare(`UPDATE conversations SET sdk_session_id = ?, updated_at = ? WHERE id = ? AND (sdk_session_id IS NULL OR sdk_session_id = '')`),
|
|
79
|
+
setRuntimeSessionSdkSessionIdIfMissing: db.prepare(`UPDATE runtime_sessions SET sdk_session_id = ?, last_used_at = ?, status = 'active' WHERE id = ? AND (sdk_session_id IS NULL OR sdk_session_id = '')`),
|
|
80
|
+
touchRuntimeSession: db.prepare(`UPDATE runtime_sessions SET model = ?, last_used_at = ?, status = 'active' WHERE id = ?`),
|
|
81
|
+
listRuntimeSessionProviderCandidates: db.prepare(`
|
|
82
|
+
SELECT
|
|
83
|
+
rs.*,
|
|
84
|
+
c.sdk_session_id AS conversation_sdk_session_id,
|
|
85
|
+
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count,
|
|
86
|
+
(SELECT COUNT(*) FROM queue q WHERE q.conversation_id = c.id AND q.status IN ('pending', 'processing', 'parked')) AS active_queue_count
|
|
87
|
+
FROM runtime_sessions rs
|
|
88
|
+
JOIN conversations c ON c.id = rs.conversation_id
|
|
89
|
+
WHERE c.status != 'deleted'
|
|
90
|
+
`),
|
|
91
|
+
updateRuntimeSessionProvider: runtimeSessionsSupportProviders
|
|
92
|
+
? db.prepare(`
|
|
93
|
+
UPDATE runtime_sessions
|
|
94
|
+
SET provider_type = ?, provider_model = ?, model = ?, last_used_at = ?, status = 'active'
|
|
95
|
+
WHERE id = ?
|
|
96
|
+
`)
|
|
97
|
+
: null,
|
|
98
|
+
updateRuntimeSessionClaudeNativeSessionId: runtimeSessionColumns.has('claude_native_session_id')
|
|
99
|
+
? db.prepare(`
|
|
100
|
+
UPDATE runtime_sessions
|
|
101
|
+
SET claude_native_session_id = ?, last_used_at = ?
|
|
102
|
+
WHERE conversation_id = ?
|
|
103
|
+
`)
|
|
104
|
+
: null,
|
|
105
|
+
updateRuntimeSessionCursorAgentId: runtimeSessionColumns.has('cursor_agent_id')
|
|
106
|
+
? db.prepare(`
|
|
107
|
+
UPDATE runtime_sessions
|
|
108
|
+
SET cursor_agent_id = ?, last_used_at = ?
|
|
109
|
+
WHERE conversation_id = ?
|
|
110
|
+
`)
|
|
111
|
+
: null,
|
|
112
|
+
updateRuntimeSessionGrokNativeSessionId: runtimeSessionColumns.has('grok_native_session_id')
|
|
113
|
+
? db.prepare(`
|
|
114
|
+
UPDATE runtime_sessions
|
|
115
|
+
SET grok_native_session_id = ?, last_used_at = ?
|
|
116
|
+
WHERE conversation_id = ?
|
|
117
|
+
`)
|
|
118
|
+
: null,
|
|
119
|
+
updateRuntimeSessionContextUsage: runtimeSessionColumns.has('context_usage_json')
|
|
120
|
+
? db.prepare(`
|
|
121
|
+
UPDATE runtime_sessions
|
|
122
|
+
SET context_usage_json = ?, context_usage_captured_at = ?
|
|
123
|
+
WHERE conversation_id = ?
|
|
124
|
+
`)
|
|
125
|
+
: null,
|
|
126
|
+
deleteRuntimeSessionByConversation: db.prepare(`DELETE FROM runtime_sessions WHERE conversation_id = ?`),
|
|
127
|
+
|
|
128
|
+
// deleted sdk sessions tombstones (hide rediscovered SDK sessions after UI delete)
|
|
129
|
+
listDeletedSdkSessions: db.prepare(`SELECT sdk_session_id FROM deleted_sdk_sessions`),
|
|
130
|
+
getDeletedSdkSession: db.prepare(`SELECT sdk_session_id FROM deleted_sdk_sessions WHERE sdk_session_id = ? LIMIT 1`),
|
|
131
|
+
upsertRelaySessionLink: db.prepare(`
|
|
132
|
+
INSERT INTO relay_session_links (sdk_session_id, conversation_id, created_at)
|
|
133
|
+
VALUES (?, ?, ?)
|
|
134
|
+
ON CONFLICT(sdk_session_id) DO UPDATE SET conversation_id = excluded.conversation_id
|
|
135
|
+
`),
|
|
136
|
+
getRelaySessionLink: db.prepare(`SELECT * FROM relay_session_links WHERE sdk_session_id = ?`),
|
|
137
|
+
markDeletedSdkSession: db.prepare(`INSERT OR REPLACE INTO deleted_sdk_sessions (sdk_session_id, deleted_at) VALUES (?, ?)`),
|
|
138
|
+
clearDeletedSdkSession: db.prepare(`DELETE FROM deleted_sdk_sessions WHERE sdk_session_id = ?`),
|
|
139
|
+
deleteDeletedSdkSessions: db.prepare(`DELETE FROM deleted_sdk_sessions`),
|
|
140
|
+
|
|
141
|
+
// recent workspace roots (relay-owned CWD history)
|
|
142
|
+
// Keyed by path_key (case-folded on Windows), so the LIMIT counts
|
|
143
|
+
// distinct directories rather than casing variants of the same one.
|
|
144
|
+
upsertRecentWorkspaceRoot: db.prepare(`
|
|
145
|
+
INSERT INTO recent_workspace_roots (path_key, path, last_seen_at)
|
|
146
|
+
VALUES (?, ?, ?)
|
|
147
|
+
ON CONFLICT(path_key) DO UPDATE SET
|
|
148
|
+
path = excluded.path,
|
|
149
|
+
last_seen_at = excluded.last_seen_at
|
|
150
|
+
`),
|
|
151
|
+
listRecentWorkspaceRoots: db.prepare(`
|
|
152
|
+
SELECT path, last_seen_at
|
|
153
|
+
FROM recent_workspace_roots
|
|
154
|
+
ORDER BY last_seen_at DESC
|
|
155
|
+
LIMIT ?
|
|
156
|
+
`),
|
|
157
|
+
pruneRecentWorkspaceRoots: db.prepare(`
|
|
158
|
+
DELETE FROM recent_workspace_roots
|
|
159
|
+
WHERE path_key NOT IN (
|
|
160
|
+
SELECT path_key
|
|
161
|
+
FROM recent_workspace_roots
|
|
162
|
+
ORDER BY last_seen_at DESC
|
|
163
|
+
LIMIT ?
|
|
164
|
+
)
|
|
165
|
+
`),
|
|
166
|
+
deleteRecentWorkspaceRoots: db.prepare(`DELETE FROM recent_workspace_roots`),
|
|
167
|
+
|
|
168
|
+
// app settings
|
|
169
|
+
getAppSetting: db.prepare(`
|
|
170
|
+
SELECT key, value, updated_at
|
|
171
|
+
FROM app_settings
|
|
172
|
+
WHERE key = ?
|
|
173
|
+
LIMIT 1
|
|
174
|
+
`),
|
|
175
|
+
upsertAppSetting: db.prepare(`
|
|
176
|
+
INSERT INTO app_settings (key, value, updated_at)
|
|
177
|
+
VALUES (?, ?, ?)
|
|
178
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
179
|
+
value = excluded.value,
|
|
180
|
+
updated_at = excluded.updated_at
|
|
181
|
+
`),
|
|
182
|
+
deleteAppSetting: db.prepare(`DELETE FROM app_settings WHERE key = ?`),
|
|
183
|
+
|
|
184
|
+
// SDK session delete bridge queue (server <-> extension)
|
|
185
|
+
upsertSdkDeleteRequest: db.prepare(`
|
|
186
|
+
INSERT INTO sdk_delete_requests (
|
|
187
|
+
sdk_session_id, conversation_id, status, requested_at, updated_at, processing_at, retry_count, next_attempt_at, last_error
|
|
188
|
+
) VALUES (?, ?, 'pending', ?, ?, NULL, 0, NULL, NULL)
|
|
189
|
+
ON CONFLICT(sdk_session_id) DO UPDATE SET
|
|
190
|
+
conversation_id = COALESCE(excluded.conversation_id, sdk_delete_requests.conversation_id),
|
|
191
|
+
status = 'pending',
|
|
192
|
+
requested_at = excluded.requested_at,
|
|
193
|
+
updated_at = excluded.updated_at,
|
|
194
|
+
processing_at = NULL,
|
|
195
|
+
retry_count = 0,
|
|
196
|
+
next_attempt_at = NULL,
|
|
197
|
+
last_error = NULL
|
|
198
|
+
`),
|
|
199
|
+
dequeueSdkDeleteRequest: db.prepare(`
|
|
200
|
+
SELECT sdk_session_id, conversation_id, retry_count, requested_at
|
|
201
|
+
FROM sdk_delete_requests
|
|
202
|
+
WHERE status = 'pending' AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
203
|
+
ORDER BY requested_at ASC
|
|
204
|
+
LIMIT 1
|
|
205
|
+
`),
|
|
206
|
+
setSdkDeleteRequestProcessing: db.prepare(`
|
|
207
|
+
UPDATE sdk_delete_requests
|
|
208
|
+
SET status = 'processing', processing_at = ?, updated_at = ?, last_error = NULL
|
|
209
|
+
WHERE sdk_session_id = ? AND status = 'pending'
|
|
210
|
+
`),
|
|
211
|
+
resetStaleSdkDeleteProcessing: db.prepare(`
|
|
212
|
+
UPDATE sdk_delete_requests
|
|
213
|
+
SET status = 'pending', processing_at = NULL, updated_at = ?
|
|
214
|
+
WHERE status = 'processing' AND processing_at < ?
|
|
215
|
+
`),
|
|
216
|
+
getSdkDeleteRequestBySessionId: db.prepare(`
|
|
217
|
+
SELECT sdk_session_id, conversation_id, status, retry_count, requested_at, updated_at, next_attempt_at, last_error
|
|
218
|
+
FROM sdk_delete_requests
|
|
219
|
+
WHERE sdk_session_id = ?
|
|
220
|
+
LIMIT 1
|
|
221
|
+
`),
|
|
222
|
+
setSdkDeleteRequestPendingWithError: db.prepare(`
|
|
223
|
+
UPDATE sdk_delete_requests
|
|
224
|
+
SET status = 'pending',
|
|
225
|
+
processing_at = NULL,
|
|
226
|
+
retry_count = retry_count + 1,
|
|
227
|
+
next_attempt_at = ?,
|
|
228
|
+
updated_at = ?,
|
|
229
|
+
last_error = ?
|
|
230
|
+
WHERE sdk_session_id = ?
|
|
231
|
+
`),
|
|
232
|
+
deleteSdkDeleteRequest: db.prepare(`DELETE FROM sdk_delete_requests WHERE sdk_session_id = ?`),
|
|
233
|
+
deleteSdkDeleteRequests: db.prepare(`DELETE FROM sdk_delete_requests`),
|
|
234
|
+
// SDK session import ledger
|
|
235
|
+
getSdkSessionImport: db.prepare(`
|
|
236
|
+
SELECT sdk_session_id, conversation_id, status, attempt_count, started_at, completed_at,
|
|
237
|
+
source_started_at, source_modified_at, updated_at, last_error
|
|
238
|
+
FROM sdk_session_imports WHERE sdk_session_id = ? LIMIT 1
|
|
239
|
+
`),
|
|
240
|
+
upsertSdkSessionImport: db.prepare(`
|
|
241
|
+
INSERT INTO sdk_session_imports (
|
|
242
|
+
sdk_session_id, conversation_id, status, attempt_count, started_at, completed_at,
|
|
243
|
+
source_started_at, source_modified_at, updated_at, last_error
|
|
244
|
+
)
|
|
245
|
+
VALUES (?, ?, 'pending', 0, NULL, NULL, NULL, NULL, ?, NULL)
|
|
246
|
+
ON CONFLICT(sdk_session_id) DO NOTHING
|
|
247
|
+
`),
|
|
248
|
+
claimSdkSessionImport: db.prepare(`
|
|
249
|
+
UPDATE sdk_session_imports
|
|
250
|
+
SET status = 'processing', attempt_count = attempt_count + 1, started_at = ?, updated_at = ?, last_error = NULL
|
|
251
|
+
WHERE sdk_session_id = ?
|
|
252
|
+
AND (? = 1 OR status != 'completed')
|
|
253
|
+
AND status != 'processing'
|
|
254
|
+
`),
|
|
255
|
+
completeSdkSessionImport: db.prepare(`
|
|
256
|
+
UPDATE sdk_session_imports
|
|
257
|
+
SET conversation_id = ?, status = 'completed', completed_at = ?, source_started_at = ?,
|
|
258
|
+
source_modified_at = ?, updated_at = ?, last_error = NULL
|
|
259
|
+
WHERE sdk_session_id = ?
|
|
260
|
+
`),
|
|
261
|
+
failSdkSessionImport: db.prepare(`
|
|
262
|
+
UPDATE sdk_session_imports
|
|
263
|
+
SET status = 'failed', updated_at = ?, last_error = ?
|
|
264
|
+
WHERE sdk_session_id = ?
|
|
265
|
+
`),
|
|
266
|
+
resetInterruptedSdkSessionImports: db.prepare(`
|
|
267
|
+
UPDATE sdk_session_imports
|
|
268
|
+
SET status = 'failed', updated_at = ?, last_error = 'Interrupted before import completion'
|
|
269
|
+
WHERE status = 'processing'
|
|
270
|
+
`),
|
|
271
|
+
resetSdkSessionImports: db.prepare(`DELETE FROM sdk_session_imports`),
|
|
272
|
+
listDeletedConversationsBySdkSessionId: db.prepare(`
|
|
273
|
+
SELECT id, sdk_session_id
|
|
274
|
+
FROM conversations
|
|
275
|
+
WHERE status = 'deleted' AND sdk_session_id = ?
|
|
276
|
+
`),
|
|
277
|
+
// public conversation shares
|
|
278
|
+
getConversationShareByConversationId: db.prepare(`
|
|
279
|
+
SELECT token, conversation_id, created_at, last_accessed_at, revoked_at
|
|
280
|
+
FROM conversation_shares
|
|
281
|
+
WHERE conversation_id = ?
|
|
282
|
+
AND (revoked_at IS NULL OR revoked_at = '')
|
|
283
|
+
ORDER BY created_at DESC
|
|
284
|
+
LIMIT 1
|
|
285
|
+
`),
|
|
286
|
+
getConversationShareByToken: db.prepare(`
|
|
287
|
+
SELECT token, conversation_id, created_at, last_accessed_at, revoked_at
|
|
288
|
+
FROM conversation_shares
|
|
289
|
+
WHERE token = ?
|
|
290
|
+
LIMIT 1
|
|
291
|
+
`),
|
|
292
|
+
insertConversationShare: db.prepare(`
|
|
293
|
+
INSERT INTO conversation_shares (
|
|
294
|
+
token,
|
|
295
|
+
conversation_id,
|
|
296
|
+
created_at,
|
|
297
|
+
last_accessed_at,
|
|
298
|
+
revoked_at
|
|
299
|
+
) VALUES (?, ?, ?, ?, NULL)
|
|
300
|
+
`),
|
|
301
|
+
touchConversationShare: db.prepare(`
|
|
302
|
+
UPDATE conversation_shares
|
|
303
|
+
SET last_accessed_at = ?
|
|
304
|
+
WHERE token = ?
|
|
305
|
+
AND (revoked_at IS NULL OR revoked_at = '')
|
|
306
|
+
`),
|
|
307
|
+
revokeConversationShareByToken: db.prepare(`
|
|
308
|
+
UPDATE conversation_shares
|
|
309
|
+
SET revoked_at = ?
|
|
310
|
+
WHERE token = ?
|
|
311
|
+
AND (revoked_at IS NULL OR revoked_at = '')
|
|
312
|
+
`),
|
|
313
|
+
revokeConversationSharesByConversationId: db.prepare(`
|
|
314
|
+
UPDATE conversation_shares
|
|
315
|
+
SET revoked_at = ?
|
|
316
|
+
WHERE conversation_id = ?
|
|
317
|
+
AND (revoked_at IS NULL OR revoked_at = '')
|
|
318
|
+
`),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared between message-repository and session-repository so the
|
|
4
|
+
// hidden-from-shares column detection and SQL stay in one place.
|
|
5
|
+
export function createShareVisibilityStatements(db) {
|
|
6
|
+
const messageColumns = new Set(
|
|
7
|
+
db.prepare(`PRAGMA table_info(messages)`).all().map((column) => String(column?.name || '').trim()),
|
|
8
|
+
);
|
|
9
|
+
const messagesSupportShareVisibility = messageColumns.has('hidden_from_shares')
|
|
10
|
+
&& messageColumns.has('share_hidden_at');
|
|
11
|
+
return {
|
|
12
|
+
messagesSupportShareVisibility,
|
|
13
|
+
getSharedMessages: db.prepare(messagesSupportShareVisibility
|
|
14
|
+
? `SELECT * FROM messages WHERE conversation_id = ? AND hidden_from_shares = 0 ORDER BY timestamp ASC`
|
|
15
|
+
: `SELECT * FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC`),
|
|
16
|
+
setMessageShareVisibility: messagesSupportShareVisibility
|
|
17
|
+
? db.prepare(`
|
|
18
|
+
UPDATE messages
|
|
19
|
+
SET hidden_from_shares = ?, share_hidden_at = ?
|
|
20
|
+
WHERE id = ? AND conversation_id = ?
|
|
21
|
+
`)
|
|
22
|
+
: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { createAskUserRoutingService } from '../services/ask-user-routing-service.mjs';
|
|
4
|
+
import {
|
|
5
|
+
extractRequestedSchema,
|
|
6
|
+
normalizeSchema,
|
|
7
|
+
schemaFields,
|
|
8
|
+
validateStructuredAnswer,
|
|
9
|
+
summarizeStructuredAnswer,
|
|
10
|
+
flatAnswerToStructured,
|
|
11
|
+
} from '../../shared/question-schema.mjs';
|
|
12
|
+
|
|
13
|
+
function normalizeId(value) {
|
|
14
|
+
const text = String(value || '').trim();
|
|
15
|
+
return text || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveNestedValue(source, keyPath) {
|
|
19
|
+
let current = source;
|
|
20
|
+
for (const key of keyPath) {
|
|
21
|
+
if (!current || typeof current !== 'object') return null;
|
|
22
|
+
current = current[key];
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function firstKnownValue(source, keyPaths = []) {
|
|
28
|
+
for (const keyPath of keyPaths) {
|
|
29
|
+
const value = resolveNestedValue(source, keyPath);
|
|
30
|
+
const normalized = normalizeId(value);
|
|
31
|
+
if (normalized) return normalized;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractContinuationOwnership(rawRequest) {
|
|
37
|
+
const parsed = rawRequest && typeof rawRequest === 'object' ? rawRequest : null;
|
|
38
|
+
if (!parsed) {
|
|
39
|
+
return {
|
|
40
|
+
continuationId: null,
|
|
41
|
+
continuationQuestionId: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
continuationId: firstKnownValue(parsed, [
|
|
47
|
+
['continuationId'],
|
|
48
|
+
['continuation_id'],
|
|
49
|
+
['continuation', 'id'],
|
|
50
|
+
['toolArgs', 'continuationId'],
|
|
51
|
+
['toolArgs', 'continuation_id'],
|
|
52
|
+
['toolArgs', 'continuation', 'id'],
|
|
53
|
+
['input', 'continuationId'],
|
|
54
|
+
['input', 'continuation_id'],
|
|
55
|
+
['input', 'continuation', 'id'],
|
|
56
|
+
['request', 'continuationId'],
|
|
57
|
+
['request', 'continuation_id'],
|
|
58
|
+
['request', 'continuation', 'id'],
|
|
59
|
+
['arguments', 'continuationId'],
|
|
60
|
+
['arguments', 'continuation_id'],
|
|
61
|
+
['arguments', 'continuation', 'id'],
|
|
62
|
+
]),
|
|
63
|
+
continuationQuestionId: firstKnownValue(parsed, [
|
|
64
|
+
['questionId'],
|
|
65
|
+
['question_id'],
|
|
66
|
+
['askUserQuestionId'],
|
|
67
|
+
['ask_user_question_id'],
|
|
68
|
+
['continuation', 'questionId'],
|
|
69
|
+
['continuation', 'question_id'],
|
|
70
|
+
['toolArgs', 'questionId'],
|
|
71
|
+
['toolArgs', 'question_id'],
|
|
72
|
+
['input', 'questionId'],
|
|
73
|
+
['input', 'question_id'],
|
|
74
|
+
['request', 'questionId'],
|
|
75
|
+
['request', 'question_id'],
|
|
76
|
+
['arguments', 'questionId'],
|
|
77
|
+
['arguments', 'question_id'],
|
|
78
|
+
]),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function registerAskUserRoutes(app, deps) {
|
|
83
|
+
const {
|
|
84
|
+
auth,
|
|
85
|
+
io,
|
|
86
|
+
db,
|
|
87
|
+
stmts,
|
|
88
|
+
runtimeState,
|
|
89
|
+
uuidv4,
|
|
90
|
+
ts,
|
|
91
|
+
questionExpiresAt,
|
|
92
|
+
sanitizeRelayQuestionPrompt,
|
|
93
|
+
sanitizeRelayQuestionRequest,
|
|
94
|
+
sanitizeRelayQuestionContext,
|
|
95
|
+
parseQuestionRequest,
|
|
96
|
+
normalizeQuestionChoices,
|
|
97
|
+
formatQuestionRow,
|
|
98
|
+
normalizeRelayMode,
|
|
99
|
+
DEFAULT_RELAY_MODE,
|
|
100
|
+
sessionWorkerRegistry,
|
|
101
|
+
pushDispatchService,
|
|
102
|
+
} = deps;
|
|
103
|
+
|
|
104
|
+
const continuationRoutingEnabled = runtimeState?.featureFlags?.SESSION_WORKER_CONTINUATION_ROUTING_ENABLED === true;
|
|
105
|
+
const workerRoutingEnabled = runtimeState?.featureFlags?.SESSION_WORKER_ROUTING_ENABLED === true;
|
|
106
|
+
const askUserRoutingService = createAskUserRoutingService(db, {
|
|
107
|
+
continuationRoutingEnabled,
|
|
108
|
+
sessionWorkerRegistry,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
app.get('/api/relay-questions', auth, (req, res) => {
|
|
112
|
+
const conversationId = req.query.conversationId ? String(req.query.conversationId) : null;
|
|
113
|
+
const status = String(req.query.status || 'pending').trim() || 'pending';
|
|
114
|
+
const rows = stmts.listQuestions.all(status, conversationId, conversationId);
|
|
115
|
+
res.json({ questions: rows.map(formatQuestionRow) });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
app.get('/api/relay-question/:id', auth, (req, res) => {
|
|
119
|
+
const row = stmts.getQuestion.get(req.params.id);
|
|
120
|
+
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
121
|
+
res.json({ question: formatQuestionRow(row) });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
app.post('/api/relay-question', auth, (req, res) => {
|
|
125
|
+
const { queueId, messageId, conversationId, mode, prompt, choices, request, context, allowFreeform, requestedSchema } = req.body;
|
|
126
|
+
console.log(`[${ts()}] relay-question POST: ${choices?.length ?? 0} possible answer(s)`);
|
|
127
|
+
const q = stmts.findQById.get(queueId || messageId);
|
|
128
|
+
if (!q || q.status !== 'processing') {
|
|
129
|
+
return res.status(409).json({ error: 'No active relay turn' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const effectiveMessageId = messageId || q.id;
|
|
133
|
+
|
|
134
|
+
// Check for an identical pending question (same prompt + choices) to handle retries.
|
|
135
|
+
// Do NOT reuse questions with different prompts/choices - this allows multiple
|
|
136
|
+
// concurrent ask_user calls in the same turn to each create their own question.
|
|
137
|
+
const promptText = sanitizeRelayQuestionPrompt({ prompt });
|
|
138
|
+
const normalizedChoices = normalizeQuestionChoices(choices);
|
|
139
|
+
const choicesJson = normalizedChoices.length ? JSON.stringify(normalizedChoices) : null;
|
|
140
|
+
|
|
141
|
+
const existingPending = stmts.findPendingQuestionByMessage.get(effectiveMessageId);
|
|
142
|
+
if (existingPending) {
|
|
143
|
+
const existingPrompt = String(existingPending.prompt || '');
|
|
144
|
+
const existingChoices = existingPending.choices || null;
|
|
145
|
+
const promptMatches = existingPrompt === promptText;
|
|
146
|
+
const choicesMatch = existingChoices === choicesJson;
|
|
147
|
+
|
|
148
|
+
if (promptMatches && choicesMatch) {
|
|
149
|
+
// This is likely a retry of the same question - reuse it
|
|
150
|
+
const question = formatQuestionRow(existingPending);
|
|
151
|
+
return res.json({ question, reused: true });
|
|
152
|
+
}
|
|
153
|
+
// Different question - continue to create a new one
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const relayMode = normalizeRelayMode(mode || q.relay_mode) || DEFAULT_RELAY_MODE;
|
|
157
|
+
const now = new Date().toISOString();
|
|
158
|
+
const questionId = uuidv4();
|
|
159
|
+
const requestedSessionId = normalizeId(req.body.sdk_session_id);
|
|
160
|
+
const queueOwnerSessionId = workerRoutingEnabled || continuationRoutingEnabled
|
|
161
|
+
? normalizeId(q.owner_sdk_session_id)
|
|
162
|
+
: null;
|
|
163
|
+
const sdkSessionId = requestedSessionId || queueOwnerSessionId || null;
|
|
164
|
+
const ownerWorkerId = sdkSessionId
|
|
165
|
+
? normalizeId(sessionWorkerRegistry?.getWorker?.(sdkSessionId)?.workerId)
|
|
166
|
+
: null;
|
|
167
|
+
const parsedQuestionRequest = parseQuestionRequest(request);
|
|
168
|
+
const requestJson = sanitizeRelayQuestionRequest({
|
|
169
|
+
request: parsedQuestionRequest,
|
|
170
|
+
context: sanitizeRelayQuestionContext(context),
|
|
171
|
+
allowFreeform: typeof allowFreeform === 'boolean' ? allowFreeform : (!normalizedChoices.length),
|
|
172
|
+
});
|
|
173
|
+
// Capture the elicitation requestedSchema (explicit field or nested in the request)
|
|
174
|
+
// so multi-field structured answers can be validated and rendered.
|
|
175
|
+
const resolvedSchema = normalizeSchema(requestedSchema)
|
|
176
|
+
|| extractRequestedSchema(requestedSchema)
|
|
177
|
+
|| extractRequestedSchema(parsedQuestionRequest);
|
|
178
|
+
const requestSchemaJson = resolvedSchema ? JSON.stringify(resolvedSchema) : null;
|
|
179
|
+
const continuation = continuationRoutingEnabled
|
|
180
|
+
? extractContinuationOwnership(parsedQuestionRequest)
|
|
181
|
+
: { continuationId: null, continuationQuestionId: null };
|
|
182
|
+
// questionExpiresAt normalizes: absent/junk falls back to the 8h default,
|
|
183
|
+
// explicit finite values (including 0) are honored.
|
|
184
|
+
const expiresAt = questionExpiresAt(now, req.body.timeout_ms);
|
|
185
|
+
|
|
186
|
+
stmts.insertQuestion.run(
|
|
187
|
+
questionId,
|
|
188
|
+
q.id,
|
|
189
|
+
conversationId || q.conversation_id,
|
|
190
|
+
effectiveMessageId,
|
|
191
|
+
relayMode,
|
|
192
|
+
promptText,
|
|
193
|
+
choicesJson,
|
|
194
|
+
requestJson,
|
|
195
|
+
requestSchemaJson,
|
|
196
|
+
sdkSessionId,
|
|
197
|
+
ownerWorkerId,
|
|
198
|
+
continuation.continuationId,
|
|
199
|
+
continuation.continuationQuestionId,
|
|
200
|
+
now,
|
|
201
|
+
expiresAt,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const question = formatQuestionRow(stmts.getQuestion.get(questionId));
|
|
205
|
+
const fieldCount = resolvedSchema ? schemaFields(resolvedSchema).length : 0;
|
|
206
|
+
console.log(`[${ts()}] QUESTION ${questionId.slice(0,8)} conv=${question.conversationId.slice(0,8)} mode=${relayMode} fields=${fieldCount} prompt="${promptText.slice(0,60)}"`);
|
|
207
|
+
io.emit('relay_question', { question });
|
|
208
|
+
void pushDispatchService?.notifyQuestion?.(question);
|
|
209
|
+
res.json({ question });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
app.post('/api/relay-question/:id/answer', auth, (req, res) => {
|
|
213
|
+
const id = String(req.params.id || '').trim();
|
|
214
|
+
const { answer, structuredAnswer, sdk_session_id, continuation_id, continuation_question_id } = req.body;
|
|
215
|
+
const sdkSessionId = normalizeId(sdk_session_id);
|
|
216
|
+
const continuationId = normalizeId(continuation_id);
|
|
217
|
+
const continuationQuestionId = normalizeId(continuation_question_id);
|
|
218
|
+
|
|
219
|
+
const row = stmts.getQuestion.get(id);
|
|
220
|
+
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
221
|
+
if (row.status !== 'pending') return res.status(409).json({ error: `Question already ${row.status}` });
|
|
222
|
+
|
|
223
|
+
const schema = parseQuestionRequest(row.request_schema);
|
|
224
|
+
const hasSchema = schema && typeof schema === 'object' && schemaFields(schema).length > 0;
|
|
225
|
+
|
|
226
|
+
// Resolve a structured answer object when a schema is present. Accept either
|
|
227
|
+
// an explicit `structuredAnswer` object or, for single-field schemas, the
|
|
228
|
+
// flat `answer` string mapped onto the lone field (backward compatibility).
|
|
229
|
+
let structured = null;
|
|
230
|
+
let flatText = String(answer || '').trim();
|
|
231
|
+
|
|
232
|
+
if (hasSchema) {
|
|
233
|
+
let candidate = null;
|
|
234
|
+
if (structuredAnswer && typeof structuredAnswer === 'object' && !Array.isArray(structuredAnswer)) {
|
|
235
|
+
candidate = structuredAnswer;
|
|
236
|
+
} else if (flatText) {
|
|
237
|
+
candidate = flatAnswerToStructured(schema, flatText);
|
|
238
|
+
}
|
|
239
|
+
if (!candidate) {
|
|
240
|
+
return res.status(400).json({ error: 'Structured answer required', schema });
|
|
241
|
+
}
|
|
242
|
+
const validation = validateStructuredAnswer(schema, candidate);
|
|
243
|
+
if (!validation.ok) {
|
|
244
|
+
return res.status(400).json({ error: 'Invalid structured answer', fields: validation.errors });
|
|
245
|
+
}
|
|
246
|
+
structured = validation.value;
|
|
247
|
+
flatText = summarizeStructuredAnswer(schema, structured) || flatText;
|
|
248
|
+
} else if (!flatText) {
|
|
249
|
+
return res.status(400).json({ error: 'Empty answer' });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = askUserRoutingService.routeAnswer({
|
|
253
|
+
question_id: id,
|
|
254
|
+
sdk_session_id: sdkSessionId,
|
|
255
|
+
continuation_id: continuationId,
|
|
256
|
+
continuation_question_id: continuationQuestionId,
|
|
257
|
+
answer: flatText,
|
|
258
|
+
structured_answer: structured ? JSON.stringify(structured) : null,
|
|
259
|
+
});
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
console.warn(
|
|
262
|
+
`[${ts()}] QUESTION reject id=${id.slice(0, 8)} reason=${String(result.error || 'unknown').slice(0, 64)} sid=${String(sdkSessionId || 'none').slice(0, 8)}`,
|
|
263
|
+
);
|
|
264
|
+
if (result.error === 'session mismatch' || result.error === 'owner mismatch') {
|
|
265
|
+
const targetSessionId = String(row.sdk_session_id || '').trim() || null;
|
|
266
|
+
return res.status(403).json({ error: result.error, expectedSessionId: targetSessionId });
|
|
267
|
+
}
|
|
268
|
+
return res.status(400).json({ error: result.error });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const question = formatQuestionRow(stmts.getQuestion.get(id));
|
|
272
|
+
console.log(`[${ts()}] QUESTION ${id.slice(0,8)} answered ${structured ? `fields=${Object.keys(structured).length}` : `len=${flatText.length}`}`);
|
|
273
|
+
io.emit('relay_question_updated', { question });
|
|
274
|
+
res.json({ ok: true, question });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
app.post('/api/relay-question/:id/timeout', auth, (req, res) => {
|
|
278
|
+
const row = stmts.getQuestion.get(req.params.id);
|
|
279
|
+
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
280
|
+
if (row.status !== 'pending') return res.json({ ok: true, question: formatQuestionRow(row) });
|
|
281
|
+
|
|
282
|
+
const result = stmts.timeoutQuestion.run(row.id);
|
|
283
|
+
if (result.changes > 0) {
|
|
284
|
+
const question = formatQuestionRow(stmts.getQuestion.get(row.id));
|
|
285
|
+
console.log(`[${ts()}] QUESTION ${row.id.slice(0,8)} timed out`);
|
|
286
|
+
io.emit('relay_question_updated', { question });
|
|
287
|
+
return res.json({ ok: true, question });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return res.json({ ok: true, question: formatQuestionRow(stmts.getQuestion.get(row.id)) });
|
|
291
|
+
});
|
|
292
|
+
}
|