@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,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared helpers for working with `ask_user` / elicitation `requestedSchema`
|
|
5
|
+
* forms. Used by the relay server, the web UI (via a browser copy of the same
|
|
6
|
+
* logic), and the extension bridge so single- and multi-field structured forms
|
|
7
|
+
* are handled consistently.
|
|
8
|
+
*
|
|
9
|
+
* A requestedSchema follows a restricted subset of JSON Schema:
|
|
10
|
+
* {
|
|
11
|
+
* "type": "object",
|
|
12
|
+
* "properties": {
|
|
13
|
+
* "<field>": {
|
|
14
|
+
* "type": "string" | "boolean" | "number" | "integer" | "array",
|
|
15
|
+
* "title": "...", "description": "...", "default": ...,
|
|
16
|
+
* "enum": [...], // string choices
|
|
17
|
+
* "oneOf": [{ "const": v, "title": l }],// labelled choices
|
|
18
|
+
* "format": "email" | "uri" | ...,
|
|
19
|
+
* "items": { ... } // for arrays (enum / anyOf)
|
|
20
|
+
* }
|
|
21
|
+
* },
|
|
22
|
+
* "required": ["field1", ...]
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseMaybeJson(value) {
|
|
31
|
+
if (typeof value !== 'string') return value;
|
|
32
|
+
const text = value.trim();
|
|
33
|
+
if (!text || (text[0] !== '{' && text[0] !== '[')) return value;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
} catch {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Return true when a request carries a requestedSchema, even when the schema
|
|
43
|
+
* is invalid. Callers use this to avoid treating a failed structured request
|
|
44
|
+
* as a legacy text prompt while the SDK validates or retries it.
|
|
45
|
+
*/
|
|
46
|
+
export function containsRequestedSchema(source) {
|
|
47
|
+
if (!source) return false;
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const queue = [source];
|
|
50
|
+
let depth = 0;
|
|
51
|
+
while (queue.length && depth < 5000) {
|
|
52
|
+
depth += 1;
|
|
53
|
+
const current = parseMaybeJson(queue.shift());
|
|
54
|
+
if (!isPlainObject(current)) continue;
|
|
55
|
+
if (seen.has(current)) continue;
|
|
56
|
+
seen.add(current);
|
|
57
|
+
if (current.requestedSchema !== undefined) return true;
|
|
58
|
+
for (const key of [
|
|
59
|
+
'toolArgs', 'arguments', 'args', 'input', 'payload', 'body', 'request',
|
|
60
|
+
'toolInput', 'toolCall', 'data',
|
|
61
|
+
]) {
|
|
62
|
+
if (current[key] !== undefined) queue.push(current[key]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Locate a `requestedSchema` object inside an arbitrary request/toolArgs shape.
|
|
70
|
+
* Returns the normalized schema object or null.
|
|
71
|
+
*/
|
|
72
|
+
export function extractRequestedSchema(source) {
|
|
73
|
+
if (!source) return null;
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const queue = [source];
|
|
76
|
+
let depth = 0;
|
|
77
|
+
while (queue.length && depth < 5000) {
|
|
78
|
+
depth += 1;
|
|
79
|
+
const current = parseMaybeJson(queue.shift());
|
|
80
|
+
if (!isPlainObject(current)) continue;
|
|
81
|
+
if (seen.has(current)) continue;
|
|
82
|
+
seen.add(current);
|
|
83
|
+
const candidate = parseMaybeJson(current.requestedSchema);
|
|
84
|
+
if (isPlainObject(candidate) && isPlainObject(candidate.properties)) {
|
|
85
|
+
return normalizeSchema(candidate);
|
|
86
|
+
}
|
|
87
|
+
for (const key of [
|
|
88
|
+
'toolArgs', 'arguments', 'args', 'input', 'payload', 'body', 'request',
|
|
89
|
+
'toolInput', 'toolCall', 'data',
|
|
90
|
+
]) {
|
|
91
|
+
if (current[key] !== undefined) queue.push(current[key]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Normalize a raw schema (parsing JSON strings, ensuring shape). */
|
|
98
|
+
export function normalizeSchema(raw) {
|
|
99
|
+
const schema = parseMaybeJson(raw);
|
|
100
|
+
if (!isPlainObject(schema) || !isPlainObject(schema.properties)) return null;
|
|
101
|
+
const required = Array.isArray(schema.required)
|
|
102
|
+
? schema.required.map((name) => String(name)).filter(Boolean)
|
|
103
|
+
: [];
|
|
104
|
+
return {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: schema.properties,
|
|
107
|
+
required,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeChoice(entry) {
|
|
112
|
+
if (typeof entry === 'string') {
|
|
113
|
+
const value = entry.trim();
|
|
114
|
+
return value ? { value, label: value } : null;
|
|
115
|
+
}
|
|
116
|
+
if (isPlainObject(entry)) {
|
|
117
|
+
const value = entry.const !== undefined ? entry.const : (entry.value !== undefined ? entry.value : entry.title);
|
|
118
|
+
if (value === undefined || value === null) return null;
|
|
119
|
+
const label = String(entry.title || entry.label || entry.description || value).trim();
|
|
120
|
+
return { value, label: label || String(value) };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fieldChoices(prop) {
|
|
126
|
+
const out = [];
|
|
127
|
+
if (Array.isArray(prop?.enum)) {
|
|
128
|
+
for (const e of prop.enum) {
|
|
129
|
+
const c = normalizeChoice(e);
|
|
130
|
+
if (c) out.push(c);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(prop?.oneOf)) {
|
|
134
|
+
for (const e of prop.oneOf) {
|
|
135
|
+
const c = normalizeChoice(e);
|
|
136
|
+
if (c) out.push(c);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Array item choices (multi-select)
|
|
140
|
+
const items = prop?.items;
|
|
141
|
+
if (isPlainObject(items)) {
|
|
142
|
+
if (Array.isArray(items.enum)) {
|
|
143
|
+
for (const e of items.enum) {
|
|
144
|
+
const c = normalizeChoice(e);
|
|
145
|
+
if (c) out.push(c);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(items.anyOf)) {
|
|
149
|
+
for (const e of items.anyOf) {
|
|
150
|
+
const c = normalizeChoice(e);
|
|
151
|
+
if (c) out.push(c);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Deduplicate by value
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
return out.filter((c) => {
|
|
158
|
+
const key = String(c.value);
|
|
159
|
+
if (seen.has(key)) return false;
|
|
160
|
+
seen.add(key);
|
|
161
|
+
return true;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Return an ordered list of normalized field descriptors for a schema.
|
|
167
|
+
*/
|
|
168
|
+
export function schemaFields(schema) {
|
|
169
|
+
const normalized = normalizeSchema(schema);
|
|
170
|
+
if (!normalized) return [];
|
|
171
|
+
const requiredSet = new Set(normalized.required);
|
|
172
|
+
const fields = [];
|
|
173
|
+
for (const [name, rawProp] of Object.entries(normalized.properties)) {
|
|
174
|
+
const prop = parseMaybeJson(rawProp);
|
|
175
|
+
if (!isPlainObject(prop)) continue;
|
|
176
|
+
const type = String(prop.type || 'string').toLowerCase();
|
|
177
|
+
const choices = fieldChoices(prop);
|
|
178
|
+
fields.push({
|
|
179
|
+
name,
|
|
180
|
+
type,
|
|
181
|
+
title: String(prop.title || name).trim() || name,
|
|
182
|
+
description: String(prop.description || '').trim(),
|
|
183
|
+
required: requiredSet.has(name),
|
|
184
|
+
hasDefault: Object.prototype.hasOwnProperty.call(prop, 'default'),
|
|
185
|
+
default: prop.default,
|
|
186
|
+
choices,
|
|
187
|
+
isMultiSelect: type === 'array',
|
|
188
|
+
format: String(prop.format || '').trim() || null,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return fields;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** True when the schema describes more than one field. */
|
|
195
|
+
export function isMultiFieldSchema(schema) {
|
|
196
|
+
return schemaFields(schema).length > 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function coerceValue(field, rawValue) {
|
|
200
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
201
|
+
return { present: false, value: undefined };
|
|
202
|
+
}
|
|
203
|
+
switch (field.type) {
|
|
204
|
+
case 'boolean': {
|
|
205
|
+
if (typeof rawValue === 'boolean') return { present: true, value: rawValue };
|
|
206
|
+
const text = String(rawValue).trim().toLowerCase();
|
|
207
|
+
if (['true', 'yes', '1', 'on'].includes(text)) return { present: true, value: true };
|
|
208
|
+
if (['false', 'no', '0', 'off'].includes(text)) return { present: true, value: false };
|
|
209
|
+
return { present: true, value: undefined, error: 'expected boolean' };
|
|
210
|
+
}
|
|
211
|
+
case 'number':
|
|
212
|
+
case 'integer': {
|
|
213
|
+
const num = Number(rawValue);
|
|
214
|
+
if (!Number.isFinite(num)) return { present: true, value: undefined, error: 'expected number' };
|
|
215
|
+
if (field.type === 'integer' && !Number.isInteger(num)) {
|
|
216
|
+
return { present: true, value: Math.trunc(num) };
|
|
217
|
+
}
|
|
218
|
+
return { present: true, value: num };
|
|
219
|
+
}
|
|
220
|
+
case 'array': {
|
|
221
|
+
let arr = rawValue;
|
|
222
|
+
if (typeof rawValue === 'string') {
|
|
223
|
+
arr = rawValue.split(',').map((s) => s.trim()).filter(Boolean);
|
|
224
|
+
}
|
|
225
|
+
if (!Array.isArray(arr)) return { present: true, value: undefined, error: 'expected array' };
|
|
226
|
+
return { present: true, value: arr.map((v) => (typeof v === 'string' ? v.trim() : v)) };
|
|
227
|
+
}
|
|
228
|
+
default: {
|
|
229
|
+
return { present: true, value: String(rawValue) };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function choiceMatches(field, value) {
|
|
235
|
+
if (!field.choices.length) return true;
|
|
236
|
+
const allowed = field.choices.map((c) => String(c.value));
|
|
237
|
+
if (field.type === 'array') {
|
|
238
|
+
const list = Array.isArray(value) ? value : [value];
|
|
239
|
+
return list.every((v) => allowed.includes(String(v)));
|
|
240
|
+
}
|
|
241
|
+
return allowed.includes(String(value));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validate and coerce a structured answer object against the schema.
|
|
246
|
+
* Returns { ok, errors: [{field, message}], value: {field: coerced} }.
|
|
247
|
+
*/
|
|
248
|
+
export function validateStructuredAnswer(schema, answer) {
|
|
249
|
+
const fields = schemaFields(schema);
|
|
250
|
+
const errors = [];
|
|
251
|
+
const value = {};
|
|
252
|
+
const source = isPlainObject(answer) ? answer : {};
|
|
253
|
+
|
|
254
|
+
for (const field of fields) {
|
|
255
|
+
let raw = source[field.name];
|
|
256
|
+
const coerced = coerceValue(field, raw);
|
|
257
|
+
|
|
258
|
+
if (!coerced.present) {
|
|
259
|
+
if (field.hasDefault) {
|
|
260
|
+
value[field.name] = field.default;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (field.required) {
|
|
264
|
+
errors.push({ field: field.name, message: `${field.title} is required` });
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (coerced.error) {
|
|
270
|
+
errors.push({ field: field.name, message: `${field.title}: ${coerced.error}` });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!choiceMatches(field, coerced.value)) {
|
|
275
|
+
errors.push({ field: field.name, message: `${field.title}: value not allowed` });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
value[field.name] = coerced.value;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { ok: errors.length === 0, errors, value };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Render a structured answer object as a short human-readable string. */
|
|
286
|
+
export function summarizeStructuredAnswer(schema, answer) {
|
|
287
|
+
const fields = schemaFields(schema);
|
|
288
|
+
const source = isPlainObject(answer) ? answer : {};
|
|
289
|
+
const parts = [];
|
|
290
|
+
const ordered = fields.length ? fields.map((f) => f.name) : Object.keys(source);
|
|
291
|
+
for (const name of ordered) {
|
|
292
|
+
if (!(name in source)) continue;
|
|
293
|
+
const field = fields.find((f) => f.name === name);
|
|
294
|
+
const label = field ? field.title : name;
|
|
295
|
+
let val = source[name];
|
|
296
|
+
if (Array.isArray(val)) val = val.join(', ');
|
|
297
|
+
parts.push(`${label}: ${val}`);
|
|
298
|
+
}
|
|
299
|
+
return parts.join(' · ');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Build a structured content object from a single flat string answer.
|
|
304
|
+
* Used for backward compatibility / single-field schemas where the relay only
|
|
305
|
+
* captured one string. Maps the answer onto the schema's single field.
|
|
306
|
+
*/
|
|
307
|
+
export function flatAnswerToStructured(schema, flatAnswer) {
|
|
308
|
+
const fields = schemaFields(schema);
|
|
309
|
+
if (fields.length !== 1) return null;
|
|
310
|
+
const field = fields[0];
|
|
311
|
+
const coerced = coerceValue(field, flatAnswer);
|
|
312
|
+
if (!coerced.present || coerced.error) return null;
|
|
313
|
+
if (!choiceMatches(field, coerced.value)) return null;
|
|
314
|
+
return { [field.name]: coerced.value };
|
|
315
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const DEFAULT_QUESTION_TIMEOUT_MS = 8 * 60 * 60_000;
|
|
2
|
+
export const QUESTION_TIMEOUT_CONTINUATION_TEXT = "[No user response before timeout — continue according to the current relay mode.]";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Absolute expiry for a relay question. Only an actual finite number is
|
|
6
|
+
* honored (including 0); anything else — absent, `null`, strings, junk —
|
|
7
|
+
* falls back to the default. The strictness is deliberate: coercing here is
|
|
8
|
+
* how `Number(null) === 0` silently produced questions that expired the
|
|
9
|
+
* moment they were created (the pre-2026-09 relay-question-ui e2e flake),
|
|
10
|
+
* and for a timeout the safe failure direction is "lives 8h", never
|
|
11
|
+
* "expires now".
|
|
12
|
+
*/
|
|
13
|
+
export function questionExpiresAt(createdAt, timeoutMs = DEFAULT_QUESTION_TIMEOUT_MS) {
|
|
14
|
+
const normalizedTimeoutMs = Number.isFinite(timeoutMs)
|
|
15
|
+
? Math.max(0, Math.trunc(timeoutMs))
|
|
16
|
+
: DEFAULT_QUESTION_TIMEOUT_MS;
|
|
17
|
+
return new Date(new Date(createdAt).getTime() + normalizedTimeoutMs).toISOString();
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The single emit-gating rule every worker's stream channel uses.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the gating the Copilot extension's stream publisher applies, so
|
|
4
|
+
// `relay_stream` traffic stays comparable across providers and engines: a
|
|
5
|
+
// conversation run on Claude, Cursor, Grok or Copilot produces the same shape
|
|
6
|
+
// of incremental updates for the same prose. It was copied byte-for-byte into
|
|
7
|
+
// four normalizers before it lived here; keep it in one place so a change to
|
|
8
|
+
// the flood control lands everywhere at once.
|
|
9
|
+
//
|
|
10
|
+
// Returns true when `nextText` is worth publishing given the last text that
|
|
11
|
+
// was published:
|
|
12
|
+
// - never publish empty text;
|
|
13
|
+
// - always publish the first text;
|
|
14
|
+
// - never re-publish identical text;
|
|
15
|
+
// - publish once 24+ new characters accumulated, or on a sentence/line
|
|
16
|
+
// boundary, or whenever the text shrank or was rewritten.
|
|
17
|
+
export function shouldEmitStreamUpdate(nextText, previousText) {
|
|
18
|
+
const next = String(nextText || '');
|
|
19
|
+
const prev = String(previousText || '');
|
|
20
|
+
if (!next) return false;
|
|
21
|
+
if (!prev) return true;
|
|
22
|
+
if (next === prev) return false;
|
|
23
|
+
const delta = next.length - prev.length;
|
|
24
|
+
if (delta >= 24) return true;
|
|
25
|
+
if (delta > 0 && /[\n.!?:)]$/.test(next)) return true;
|
|
26
|
+
if (delta <= 0) return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a provider-reported subagent call id before it becomes a database
|
|
3
|
+
* row id. Malformed ids have been seen live (an embedded newline; two
|
|
4
|
+
* concatenated tool ids): they round-trip into URLs and DOM lookups, and while
|
|
5
|
+
* the client escapes them, a control character inside a primary key helps
|
|
6
|
+
* nobody. Non-printable runs collapse to a single dash and the id is capped.
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeSubagentRunId(raw) {
|
|
9
|
+
const trimmed = String(raw || '').trim();
|
|
10
|
+
if (!trimmed) return null;
|
|
11
|
+
const cleaned = trimmed
|
|
12
|
+
.replace(/[^\x21-\x7E]+/g, '-')
|
|
13
|
+
.replace(/-{2,}/g, '-')
|
|
14
|
+
.replace(/^-+|-+$/g, '');
|
|
15
|
+
if (!cleaned) return null;
|
|
16
|
+
return cleaned.slice(0, 128);
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The per-thought character cap shared by every worker's reasoning channel.
|
|
2
|
+
//
|
|
3
|
+
// A single reasoning block can run to hundreds of kilobytes; the relay stores
|
|
4
|
+
// and re-broadcasts every thought update, so an uncapped thought turns one
|
|
5
|
+
// verbose turn into a transport problem. 16 KiB is the cap the Copilot
|
|
6
|
+
// extension's reasoning-stream bridge has used since it shipped, and the
|
|
7
|
+
// worker normalizers copied it verbatim — this is that one definition.
|
|
8
|
+
export const MAX_THOUGHT_CHARS = 16 * 1024;
|
|
9
|
+
|
|
10
|
+
export function capThought(text) {
|
|
11
|
+
const value = String(text || '');
|
|
12
|
+
return value.length <= MAX_THOUGHT_CHARS ? value : value.slice(0, MAX_THOUGHT_CHARS);
|
|
13
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const DEFAULT_MAX_LENGTH = 140;
|
|
2
|
+
|
|
3
|
+
export function parseToolPayload(value) {
|
|
4
|
+
if (typeof value !== 'string') return value;
|
|
5
|
+
const text = value.trim();
|
|
6
|
+
if (!text || (!text.startsWith('{') && !text.startsWith('['))) return value;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(text);
|
|
9
|
+
} catch {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeToolActivityText(value, maxLength = DEFAULT_MAX_LENGTH) {
|
|
15
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
16
|
+
if (!text) return '';
|
|
17
|
+
return text.slice(0, Math.max(1, Number(maxLength) || DEFAULT_MAX_LENGTH));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatToolFieldValue(value) {
|
|
21
|
+
if (value === null || value === undefined) return '';
|
|
22
|
+
if (typeof value === 'string') return value;
|
|
23
|
+
if (Array.isArray(value)) return value.join(', ');
|
|
24
|
+
if (typeof value === 'object') {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
} catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatStoreMemoryActivity(toolName, args) {
|
|
35
|
+
const parsedArgs = parseToolPayload(args);
|
|
36
|
+
if (!parsedArgs || typeof parsedArgs !== 'object' || Array.isArray(parsedArgs)) return '';
|
|
37
|
+
const fields = ['subject', 'fact', 'citations', 'reason', 'scope']
|
|
38
|
+
.map((key) => {
|
|
39
|
+
const value = formatToolFieldValue(parsedArgs[key]).replace(/\s+/g, ' ').trim();
|
|
40
|
+
return value ? `${key}="${value.replace(/"/g, "'")}"` : '';
|
|
41
|
+
})
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
if (!fields.length) return `Tool (${String(toolName || 'store_memory').trim() || 'store_memory'})`;
|
|
44
|
+
return `Tool (${String(toolName || 'store_memory').trim() || 'store_memory'}):\n${fields.join('\n')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatVoteMemoryActivity(toolName, args) {
|
|
48
|
+
const parsedArgs = parseToolPayload(args);
|
|
49
|
+
if (!parsedArgs || typeof parsedArgs !== 'object' || Array.isArray(parsedArgs)) return '';
|
|
50
|
+
const fields = ['fact', 'direction', 'reason', 'scope']
|
|
51
|
+
.map((key) => {
|
|
52
|
+
const value = formatToolFieldValue(parsedArgs[key]).replace(/\s+/g, ' ').trim();
|
|
53
|
+
return value ? `${key}="${value.replace(/"/g, "'")}"` : '';
|
|
54
|
+
})
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
if (!fields.length) return `Tool (${String(toolName || 'vote_memory').trim() || 'vote_memory'})`;
|
|
57
|
+
return `Tool (${String(toolName || 'vote_memory').trim() || 'vote_memory'}):\n${fields.join('\n')}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function extractToolResultText(value, maxLength = DEFAULT_MAX_LENGTH) {
|
|
61
|
+
const seen = new WeakSet();
|
|
62
|
+
const candidates = [];
|
|
63
|
+
function collect(input) {
|
|
64
|
+
const parsed = parseToolPayload(input);
|
|
65
|
+
if (parsed === null || parsed === undefined) return;
|
|
66
|
+
if (typeof parsed === 'string') {
|
|
67
|
+
const text = normalizeToolActivityText(parsed, maxLength);
|
|
68
|
+
if (text) candidates.push(text);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof parsed !== 'object') return;
|
|
72
|
+
if (seen.has(parsed)) return;
|
|
73
|
+
seen.add(parsed);
|
|
74
|
+
for (const key of ['value', 'text', 'content', 'output_text', 'outputText', 'summary', 'title', 'url']) {
|
|
75
|
+
if (parsed[key] !== undefined) collect(parsed[key]);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(parsed)) {
|
|
78
|
+
for (const item of parsed) collect(item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
collect(value);
|
|
82
|
+
return candidates.find(Boolean) || '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatToolResultActivity(toolName, result, maxLength = DEFAULT_MAX_LENGTH) {
|
|
86
|
+
const name = String(toolName || '').trim();
|
|
87
|
+
if (!name) return '';
|
|
88
|
+
const lower = name.toLowerCase();
|
|
89
|
+
const text = extractToolResultText(result, maxLength);
|
|
90
|
+
if (!text) return '';
|
|
91
|
+
if (lower.includes('web_search') || lower.includes('web search')) {
|
|
92
|
+
return `Tool (${name}): output="${text.replace(/"/g, "'")}"`;
|
|
93
|
+
}
|
|
94
|
+
if (lower.includes('web_fetch') || lower.includes('web fetch')) {
|
|
95
|
+
return `Tool (${name}): output="${text.replace(/"/g, "'")}"`;
|
|
96
|
+
}
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Absolute cap on how long a single turn may stay in 'processing' before the
|
|
2
|
+
// relay requeues it, shared so the settings slider and the recovery sweep agree
|
|
3
|
+
// on bounds and formatting.
|
|
4
|
+
//
|
|
5
|
+
// This is only a backstop. Normal staleness is measured from worker heartbeats
|
|
6
|
+
// (see listRecoverableProcessing), and a turn waiting on an unanswered question
|
|
7
|
+
// is exempt from both — so the ceiling exists purely for a worker that is hung
|
|
8
|
+
// while still heartbeating.
|
|
9
|
+
|
|
10
|
+
export const TURN_CEILING_MIN_MINUTES = 0;
|
|
11
|
+
export const TURN_CEILING_MAX_MINUTES = 600;
|
|
12
|
+
export const TURN_CEILING_STEP_MINUTES = 5;
|
|
13
|
+
export const DEFAULT_TURN_CEILING_MINUTES = 60;
|
|
14
|
+
|
|
15
|
+
/** 0 disables the ceiling entirely. */
|
|
16
|
+
export function normalizeTurnCeilingMinutes(value, fallback = DEFAULT_TURN_CEILING_MINUTES) {
|
|
17
|
+
// Number('') and Number(null) are both 0, which would silently read as
|
|
18
|
+
// "no limit". Absent input is absent, not a request to disable the ceiling.
|
|
19
|
+
if (value === null || value === undefined) return fallback;
|
|
20
|
+
if (typeof value === 'string' && value.trim() === '') return fallback;
|
|
21
|
+
const numeric = Number(value);
|
|
22
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
23
|
+
const rounded = Math.round(numeric);
|
|
24
|
+
if (rounded <= TURN_CEILING_MIN_MINUTES) return 0;
|
|
25
|
+
return Math.min(TURN_CEILING_MAX_MINUTES, rounded);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function turnCeilingMinutesToMs(minutes) {
|
|
29
|
+
return normalizeTurnCeilingMinutes(minutes) * 60_000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** An unset setting means "never configured", which is the default — not 0. */
|
|
33
|
+
export function readTurnCeilingSetting(storedValue) {
|
|
34
|
+
const text = storedValue === null || storedValue === undefined ? '' : String(storedValue).trim();
|
|
35
|
+
if (!text) return DEFAULT_TURN_CEILING_MINUTES;
|
|
36
|
+
return normalizeTurnCeilingMinutes(text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseTurnCeilingUpdate(value) {
|
|
40
|
+
const minutes = normalizeTurnCeilingMinutes(value, null);
|
|
41
|
+
if (minutes === null) return { ok: false, error: 'ceilingMinutes must be a number' };
|
|
42
|
+
return { ok: true, minutes };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatTurnCeilingLabel(minutes) {
|
|
46
|
+
const normalized = normalizeTurnCeilingMinutes(minutes);
|
|
47
|
+
if (normalized === 0) return 'No limit';
|
|
48
|
+
if (normalized < 60) return `${normalized} min`;
|
|
49
|
+
const hours = Math.floor(normalized / 60);
|
|
50
|
+
const remainder = normalized % 60;
|
|
51
|
+
const hourLabel = `${hours} h`;
|
|
52
|
+
return remainder ? `${hourLabel} ${remainder} min` : hourLabel;
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The two things every session worker's entry point needs before it can do
|
|
2
|
+
// anything: which relay session it was spawned for, and a debug channel that
|
|
3
|
+
// timestamps and tags its own lines.
|
|
4
|
+
//
|
|
5
|
+
// Used by the Copilot SDK worker today. The Claude/Cursor/Grok workers still
|
|
6
|
+
// carry their own copies of both; folding them in is a mechanical follow-up
|
|
7
|
+
// (tracked in docs/plans/copilot-sdk-worker.md) that is deliberately not part
|
|
8
|
+
// of the dormant worker's review pass — those three are live.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The relay session id, from `--session-id <id>`, `--session-id=<id>`, or the
|
|
12
|
+
* `SESSION_ID` environment variable, in that order. Returns '' when none is
|
|
13
|
+
* present so the caller can fail with its own message.
|
|
14
|
+
*/
|
|
15
|
+
export function parseSessionIdArg(argv = process.argv, env = process.env) {
|
|
16
|
+
const index = argv.indexOf('--session-id');
|
|
17
|
+
if (index !== -1 && argv[index + 1]) return String(argv[index + 1]).trim();
|
|
18
|
+
const inline = argv.find((arg) => String(arg || '').startsWith('--session-id='));
|
|
19
|
+
if (inline) return String(inline.split('=')[1] || '').trim();
|
|
20
|
+
return String(env.SESSION_ID || '').trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A `dbg(...parts)` that prefixes every line with the worker name and an ISO
|
|
25
|
+
* timestamp, so interleaved worker logs stay attributable in a shared journal.
|
|
26
|
+
*/
|
|
27
|
+
export function createWorkerDebug(workerName, { log = console.log } = {}) {
|
|
28
|
+
const name = String(workerName || 'worker').trim() || 'worker';
|
|
29
|
+
return (...parts) => {
|
|
30
|
+
log(`[${name} ${new Date().toISOString()}]`, ...parts);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A numeric env override that honours 0 as a real value (the project's
|
|
36
|
+
* "0 = no limit / disabled" convention), returning undefined when unset or
|
|
37
|
+
* unparseable so an option default can take over.
|
|
38
|
+
*/
|
|
39
|
+
export function readOptionalMs(name, env = process.env) {
|
|
40
|
+
const raw = String(env[name] || '').trim();
|
|
41
|
+
if (!raw) return undefined;
|
|
42
|
+
const parsed = Number(raw);
|
|
43
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-resort crash handling for provider session workers.
|
|
3
|
+
*
|
|
4
|
+
* Without these handlers an uncaught exception (or, on modern Node, an
|
|
5
|
+
* unhandled rejection) kills the worker silently mid-turn: no requeue, no
|
|
6
|
+
* terminal response, just a 'processing' row waiting out the server's
|
|
7
|
+
* dead-worker recovery. The guard requeues every queue row the worker owes
|
|
8
|
+
* work for (continuation rows are torn down server-side by the same route),
|
|
9
|
+
* then exits non-zero so the supervisor sees a real failure.
|
|
10
|
+
*
|
|
11
|
+
* The requeue is strictly bounded: a crash handler that hangs is worse than
|
|
12
|
+
* no handler at all.
|
|
13
|
+
*/
|
|
14
|
+
export function installWorkerCrashGuard({
|
|
15
|
+
api,
|
|
16
|
+
workerName = 'session-worker',
|
|
17
|
+
getActiveQueueMessageIds = () => [],
|
|
18
|
+
onBeforeExit = () => {},
|
|
19
|
+
requeueTimeoutMs = 2_000,
|
|
20
|
+
exit = (code) => process.exit(code),
|
|
21
|
+
processImpl = process,
|
|
22
|
+
logError = (...args) => console.error(...args),
|
|
23
|
+
} = {}) {
|
|
24
|
+
let crashing = false;
|
|
25
|
+
const handler = (kind) => (error) => {
|
|
26
|
+
if (crashing) return;
|
|
27
|
+
crashing = true;
|
|
28
|
+
logError(`${workerName} ${kind}:`, error?.stack || error?.message || error);
|
|
29
|
+
(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const ids = [...new Set(
|
|
32
|
+
(getActiveQueueMessageIds() || [])
|
|
33
|
+
.map((id) => String(id || '').trim())
|
|
34
|
+
.filter(Boolean),
|
|
35
|
+
)];
|
|
36
|
+
if (ids.length && typeof api === 'function') {
|
|
37
|
+
await Promise.race([
|
|
38
|
+
Promise.allSettled(ids.map((id) => api('POST', '/api/requeue', { messageId: id }))),
|
|
39
|
+
new Promise((resolve) => {
|
|
40
|
+
const timer = setTimeout(resolve, Math.max(0, Number(requeueTimeoutMs) || 0));
|
|
41
|
+
timer.unref?.();
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
try { onBeforeExit(); } catch {}
|
|
47
|
+
exit(1);
|
|
48
|
+
})();
|
|
49
|
+
};
|
|
50
|
+
const onException = handler('uncaughtException');
|
|
51
|
+
const onRejection = handler('unhandledRejection');
|
|
52
|
+
processImpl.on('uncaughtException', onException);
|
|
53
|
+
processImpl.on('unhandledRejection', onRejection);
|
|
54
|
+
return {
|
|
55
|
+
uninstall() {
|
|
56
|
+
processImpl.off('uncaughtException', onException);
|
|
57
|
+
processImpl.off('unhandledRejection', onRejection);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|