@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,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Grok session worker: a per-conversation Node process that speaks the same
|
|
3
|
+
// relay contracts as the Claude/Cursor workers but executes turns through the
|
|
4
|
+
// Grok CLI ACP surface (grok agent stdio).
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
loadTokenFromConfig,
|
|
10
|
+
resolveRelayServerUrl,
|
|
11
|
+
} from '../../.github/extensions/web-relay/runtime/config-loader.mjs';
|
|
12
|
+
import { createApiClient } from '../../.github/extensions/web-relay/runtime/api-client.mjs';
|
|
13
|
+
import { createWorkerWebSocketLink } from '../../.github/extensions/web-relay/runtime/worker-websocket-link.mjs';
|
|
14
|
+
import { createHeartbeatController } from '../../.github/extensions/web-relay/polling/heartbeat.mjs';
|
|
15
|
+
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
16
|
+
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
17
|
+
import { createGrokTurnRunner } from './grok-turn-runner.mjs';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const HEARTBEAT_MS = 10_000;
|
|
21
|
+
|
|
22
|
+
function parseSessionIdArg(argv = process.argv) {
|
|
23
|
+
const index = argv.indexOf('--session-id');
|
|
24
|
+
if (index !== -1 && argv[index + 1]) return String(argv[index + 1]).trim();
|
|
25
|
+
const inline = argv.find((arg) => String(arg || '').startsWith('--session-id='));
|
|
26
|
+
if (inline) return String(inline.split('=')[1] || '').trim();
|
|
27
|
+
return String(process.env.SESSION_ID || '').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dbg(...parts) {
|
|
31
|
+
const timestamp = new Date().toISOString();
|
|
32
|
+
console.log(`[grok-worker ${timestamp}]`, ...parts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function main() {
|
|
36
|
+
const sdkSessionId = parseSessionIdArg();
|
|
37
|
+
if (!sdkSessionId) {
|
|
38
|
+
console.error('grok-session-worker: missing --session-id');
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
43
|
+
|| path.resolve(__dirname, '..', 'config.json');
|
|
44
|
+
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
45
|
+
const token = loadTokenFromConfig(configPath);
|
|
46
|
+
const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
|
|
47
|
+
const defaultModel = String(process.env.GROK_RELAY_MODEL || '').trim();
|
|
48
|
+
const command = String(process.env.GROK_CLI_COMMAND || process.env.GROK_COMMAND || 'grok').trim() || 'grok';
|
|
49
|
+
const alwaysApprove = String(process.env.GROK_ALWAYS_APPROVE || '1').trim() !== '0';
|
|
50
|
+
|
|
51
|
+
const api = createApiClient({
|
|
52
|
+
serverUrl,
|
|
53
|
+
token,
|
|
54
|
+
getHeaders: () => ({
|
|
55
|
+
'X-Relay-Process-Pid': String(process.pid),
|
|
56
|
+
'X-Relay-Parent-Pid': String(process.ppid),
|
|
57
|
+
'X-Relay-Session-Id': sdkSessionId,
|
|
58
|
+
'X-Relay-Conversation-Id': sdkSessionId,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const controlPoller = createControlPoller({
|
|
63
|
+
api,
|
|
64
|
+
sdkSessionId,
|
|
65
|
+
abortAckNote: 'grok run cancelled',
|
|
66
|
+
dbg,
|
|
67
|
+
});
|
|
68
|
+
const turnRunner = createGrokTurnRunner({
|
|
69
|
+
api,
|
|
70
|
+
sdkSessionId,
|
|
71
|
+
cwd,
|
|
72
|
+
defaultModel,
|
|
73
|
+
command,
|
|
74
|
+
alwaysApprove,
|
|
75
|
+
controlPoller,
|
|
76
|
+
dbg,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let heartbeatTimer = null;
|
|
80
|
+
const heartbeat = createHeartbeatController({
|
|
81
|
+
api,
|
|
82
|
+
pollMs: HEARTBEAT_MS,
|
|
83
|
+
getSessionReady: () => true,
|
|
84
|
+
getHeartbeatTimer: () => heartbeatTimer,
|
|
85
|
+
setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
|
|
86
|
+
getActiveQueueMessageId: () => turnRunner.getActiveQueueMessageId(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const wsLink = createWorkerWebSocketLink({
|
|
90
|
+
serverUrl,
|
|
91
|
+
token,
|
|
92
|
+
dbg,
|
|
93
|
+
getSessionReady: () => true,
|
|
94
|
+
getSessionId: () => sdkSessionId,
|
|
95
|
+
getPid: () => process.pid,
|
|
96
|
+
onDeliver: async (pending, reason) => {
|
|
97
|
+
dbg('queue.deliver received', `reason=${reason}`, `msgId=${pending?.message?.id || 'none'}`);
|
|
98
|
+
const deliveredCeiling = Number(pending?.settings?.turnCeilingMs);
|
|
99
|
+
if (Number.isFinite(deliveredCeiling) && deliveredCeiling >= 0) {
|
|
100
|
+
turnRunner.setTurnCeilingMs?.(deliveredCeiling);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return await turnRunner.handlePendingPayload(pending);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
dbg('turn handling failed', error?.message || String(error));
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const shutdown = async (signal) => {
|
|
112
|
+
dbg(`shutting down (${signal})`);
|
|
113
|
+
try { wsLink.stop(); } catch {}
|
|
114
|
+
try { heartbeat.stopHeartbeat(); } catch {}
|
|
115
|
+
try { controlPoller.stop(); } catch {}
|
|
116
|
+
// Bounded: a hung ACP agent teardown must never leave the process
|
|
117
|
+
// ignoring the supervisor's SIGTERM.
|
|
118
|
+
try {
|
|
119
|
+
await Promise.race([
|
|
120
|
+
turnRunner.dispose(),
|
|
121
|
+
new Promise((resolve) => {
|
|
122
|
+
const timer = setTimeout(resolve, 3_000);
|
|
123
|
+
timer.unref?.();
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
} catch {}
|
|
127
|
+
process.exit(0);
|
|
128
|
+
};
|
|
129
|
+
process.on('SIGTERM', () => { shutdown('SIGTERM'); });
|
|
130
|
+
process.on('SIGINT', () => { shutdown('SIGINT'); });
|
|
131
|
+
installWorkerCrashGuard({
|
|
132
|
+
api,
|
|
133
|
+
workerName: 'grok-session-worker',
|
|
134
|
+
getActiveQueueMessageIds: () => [turnRunner.getActiveQueueMessageId()],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
138
|
+
heartbeat.startHeartbeat();
|
|
139
|
+
wsLink.start();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch((error) => {
|
|
143
|
+
console.error('grok-session-worker fatal:', error?.stack || error?.message || error);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGrokAgentHandle,
|
|
3
|
+
startGrokTurn,
|
|
4
|
+
classifyGrokError,
|
|
5
|
+
} from './grok-sdk-adapter.mjs';
|
|
6
|
+
import { buildGrokContextUsage, resolveGrokContextWindow } from './grok-context-usage.mjs';
|
|
7
|
+
import { extractGrokUsageFromPromptResult, normalizeGrokTurnUsage } from '../services/plan-usage-grok.mjs';
|
|
8
|
+
import { createPreviewInstructionsProvider } from '../../shared/preview-instructions.mjs';
|
|
9
|
+
import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
10
|
+
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
11
|
+
|
|
12
|
+
const PLAN_BOARD_ACTIONS = [
|
|
13
|
+
{ id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
|
|
14
|
+
{ id: 'interactive', label: 'Stop here and prompt myself', mode: 'agent' },
|
|
15
|
+
{ id: 'exit_only', label: 'Stop here', mode: 'agent' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Grok has no exit-plan tool, so the only board source is the plan-mode
|
|
19
|
+
// text-shape fallback; the payload matches the Claude/Cursor workers'.
|
|
20
|
+
export function buildGrokPlanReadyBoardPayload({
|
|
21
|
+
message,
|
|
22
|
+
planText = '',
|
|
23
|
+
source = 'plan-mode-fallback',
|
|
24
|
+
} = {}) {
|
|
25
|
+
const summary = String(planText || '').trim();
|
|
26
|
+
if (!summary) return null;
|
|
27
|
+
return {
|
|
28
|
+
queueId: message?.id,
|
|
29
|
+
messageId: message?.id,
|
|
30
|
+
conversationId: message?.conversationId,
|
|
31
|
+
mode: message?.relayMode || 'agent',
|
|
32
|
+
boardType: 'plan_ready',
|
|
33
|
+
title: 'Plan ready for review',
|
|
34
|
+
body: summary,
|
|
35
|
+
actions: PLAN_BOARD_ACTIONS,
|
|
36
|
+
recommendedAction: null,
|
|
37
|
+
context: {
|
|
38
|
+
source,
|
|
39
|
+
queueMessageId: message?.id || null,
|
|
40
|
+
conversationId: message?.conversationId || null,
|
|
41
|
+
relayMode: message?.relayMode || 'agent',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildGrokTerminalError(classified, message) {
|
|
47
|
+
return {
|
|
48
|
+
kind: 'grok-turn-failed',
|
|
49
|
+
code: classified.code,
|
|
50
|
+
stableCode: classified.code,
|
|
51
|
+
message: classified.message,
|
|
52
|
+
failedAt: new Date().toISOString(),
|
|
53
|
+
queueMessageId: String(message?.id || '') || null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const MODE_NUDGES = {
|
|
58
|
+
ask: '[Relay mode: ask] Prioritize clarification questions before implementation work; '
|
|
59
|
+
+ 'do not make broad assumptions when a question would materially change the result.',
|
|
60
|
+
autopilot: '[Relay mode: autopilot] Keep moving unless user input is truly blocking; avoid unnecessary questions.',
|
|
61
|
+
plan: '[Relay mode: plan] Produce a concrete plan before making code changes; prefer outlining steps over implementing.',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function grokModeNudge(relayMode, previousMode = '') {
|
|
65
|
+
const mode = String(relayMode || 'agent').trim().toLowerCase();
|
|
66
|
+
const previous = String(previousMode || '').trim().toLowerCase();
|
|
67
|
+
if (mode === previous) return '';
|
|
68
|
+
if (MODE_NUDGES[mode]) return MODE_NUDGES[mode];
|
|
69
|
+
if (MODE_NUDGES[previous]) return `[Relay mode: ${mode}] Previous relay-mode instructions no longer apply.`;
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildGrokUserText(message = {}) {
|
|
74
|
+
const text = String(message?.text || message?.content || '').trim();
|
|
75
|
+
const attachments = Array.isArray(message?.attachments) ? message.attachments : [];
|
|
76
|
+
const notes = [];
|
|
77
|
+
for (const attachment of attachments) {
|
|
78
|
+
const name = String(attachment?.name || attachment?.filename || '').trim();
|
|
79
|
+
const pathValue = String(attachment?.path || attachment?.absolutePath || '').trim();
|
|
80
|
+
if (pathValue) notes.push(`[Attached file: ${pathValue}]`);
|
|
81
|
+
else if (name) notes.push(`[Attached file: ${name}]`);
|
|
82
|
+
}
|
|
83
|
+
return [text, ...notes].filter(Boolean).join('\n\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Execute one delivered relay turn against the Grok CLI ACP surface.
|
|
88
|
+
*/
|
|
89
|
+
export function createGrokTurnRunner({
|
|
90
|
+
api,
|
|
91
|
+
sdkSessionId,
|
|
92
|
+
cwd,
|
|
93
|
+
defaultModel = '',
|
|
94
|
+
command = 'grok',
|
|
95
|
+
alwaysApprove = true,
|
|
96
|
+
controlPoller,
|
|
97
|
+
createAgentHandleImpl = createGrokAgentHandle,
|
|
98
|
+
startGrokTurnImpl = startGrokTurn,
|
|
99
|
+
classifyErrorImpl = classifyGrokError,
|
|
100
|
+
getPreviewInstructions = createPreviewInstructionsProvider({ api }),
|
|
101
|
+
dbg = () => {},
|
|
102
|
+
} = {}) {
|
|
103
|
+
let agentHandle = null;
|
|
104
|
+
let grokNativeSessionId = '';
|
|
105
|
+
let activeMessage = null;
|
|
106
|
+
let waitingForTurn = false;
|
|
107
|
+
let currentAbortController = null;
|
|
108
|
+
let lastNudgedRelayMode = '';
|
|
109
|
+
// ACP has no system-prompt seam, so capability guidance rides the prompt
|
|
110
|
+
// prefix like the mode nudge — once per worker, since the Grok session keeps
|
|
111
|
+
// the history it was told in.
|
|
112
|
+
let previewInstructionsSent = false;
|
|
113
|
+
// The user's max-turn-duration ceiling (0 = no limit), piggybacked on queue
|
|
114
|
+
// deliveries; null means no delivery has told us yet and the ACP defaults
|
|
115
|
+
// apply.
|
|
116
|
+
let turnCeilingMs = null;
|
|
117
|
+
|
|
118
|
+
function getActiveQueueMessageId() {
|
|
119
|
+
return waitingForTurn ? String(activeMessage?.id || '') : '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isTurnActive() {
|
|
123
|
+
return waitingForTurn;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function dispose() {
|
|
127
|
+
try {
|
|
128
|
+
await agentHandle?.close?.();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
dbg('grok agent close failed on dispose', error?.message || String(error));
|
|
131
|
+
}
|
|
132
|
+
agentHandle = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function persistNativeSessionId(message, sessionId) {
|
|
136
|
+
const normalized = String(sessionId || '').trim();
|
|
137
|
+
if (!normalized || normalized === grokNativeSessionId) return;
|
|
138
|
+
try {
|
|
139
|
+
await api('POST', '/api/grok-native-session', {
|
|
140
|
+
conversationId: message.conversationId,
|
|
141
|
+
grokNativeSessionId: normalized,
|
|
142
|
+
});
|
|
143
|
+
grokNativeSessionId = normalized;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
dbg('grok native session persist failed', error?.message || String(error));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function ensureAgentHandle(message, model) {
|
|
150
|
+
if (agentHandle) return agentHandle;
|
|
151
|
+
const resumeId = String(message.grokNativeSessionId || grokNativeSessionId || '').trim();
|
|
152
|
+
agentHandle = await createAgentHandleImpl({
|
|
153
|
+
command,
|
|
154
|
+
cwd,
|
|
155
|
+
alwaysApprove,
|
|
156
|
+
nativeSessionId: resumeId,
|
|
157
|
+
model,
|
|
158
|
+
dbg,
|
|
159
|
+
});
|
|
160
|
+
if (agentHandle.sessionId) {
|
|
161
|
+
await persistNativeSessionId(message, agentHandle.sessionId);
|
|
162
|
+
}
|
|
163
|
+
// A silent fresh start after a failed session/load looks like the
|
|
164
|
+
// conversation lost its memory for no reason — say so in the transcript.
|
|
165
|
+
if (agentHandle.resumeFailed && message?.id) {
|
|
166
|
+
await postActivity(message, 'System note: the previous Grok session could not be restored; this turn starts a fresh agent session without the earlier context.');
|
|
167
|
+
}
|
|
168
|
+
return agentHandle;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function postActivity(message, text, subagentRunId = null) {
|
|
172
|
+
if (!text) return;
|
|
173
|
+
await api('POST', '/api/activity', {
|
|
174
|
+
messageId: message.id,
|
|
175
|
+
conversationId: message.conversationId,
|
|
176
|
+
mode: message.relayMode || 'agent',
|
|
177
|
+
text,
|
|
178
|
+
...(subagentRunId ? { subagentRunId } : {}),
|
|
179
|
+
}).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function dispatchAction(message, action, state) {
|
|
183
|
+
const { channel, payload } = action;
|
|
184
|
+
if (channel === 'init') {
|
|
185
|
+
state.responseModel = payload.model || state.responseModel;
|
|
186
|
+
await persistNativeSessionId(message, payload.sessionId);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (channel === 'stream') {
|
|
190
|
+
if (!payload.subagentRunId) state.lastStreamedText = payload.text;
|
|
191
|
+
await api('POST', '/api/stream', {
|
|
192
|
+
messageId: message.id,
|
|
193
|
+
conversationId: message.conversationId,
|
|
194
|
+
mode: message.relayMode || 'agent',
|
|
195
|
+
text: payload.text,
|
|
196
|
+
done: payload.done === true,
|
|
197
|
+
...(payload.subagentRunId ? { subagentRunId: payload.subagentRunId } : {}),
|
|
198
|
+
}).catch(() => {});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (channel === 'thought') {
|
|
202
|
+
await api('POST', '/api/thought', {
|
|
203
|
+
messageId: message.id,
|
|
204
|
+
conversationId: message.conversationId,
|
|
205
|
+
mode: message.relayMode || 'agent',
|
|
206
|
+
reasoningId: payload.reasoningId,
|
|
207
|
+
text: payload.text,
|
|
208
|
+
done: payload.done === true,
|
|
209
|
+
...(payload.subagentRunId ? { subagentRunId: payload.subagentRunId } : {}),
|
|
210
|
+
}).catch(() => {});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (channel === 'activity') {
|
|
214
|
+
await postActivity(message, payload.text, payload.subagentRunId);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (channel === 'subagent') {
|
|
218
|
+
await api('POST', '/api/subagent-run', {
|
|
219
|
+
messageId: message.id,
|
|
220
|
+
conversationId: message.conversationId,
|
|
221
|
+
subagentRunId: payload.subagentRunId,
|
|
222
|
+
parentSubagentId: payload.parentSubagentId || undefined,
|
|
223
|
+
displayName: payload.displayName || undefined,
|
|
224
|
+
status: payload.status,
|
|
225
|
+
}).catch(() => {});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (channel === 'result') {
|
|
229
|
+
state.result = payload;
|
|
230
|
+
state.responseModel = payload.model || state.responseModel;
|
|
231
|
+
// Prefer the prompt-result usage blob; fall back to a normalized stream usage.
|
|
232
|
+
if (payload?.usage) {
|
|
233
|
+
state.turnUsage = normalizeGrokTurnUsage(payload.usage)
|
|
234
|
+
|| extractGrokUsageFromPromptResult({ _meta: payload.usage })
|
|
235
|
+
|| state.turnUsage;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Ship the turn's token usage to the relay as a context-window breakdown.
|
|
242
|
+
* Advisory data: a failure here must not disturb the turn's response.
|
|
243
|
+
*/
|
|
244
|
+
async function publishContextUsage(message, state, model) {
|
|
245
|
+
if (!state.turnUsage) return;
|
|
246
|
+
const usageModel = state.responseModel || state.turnUsage.modelId || model || '';
|
|
247
|
+
const contextWindow = resolveGrokContextWindow(
|
|
248
|
+
usageModel,
|
|
249
|
+
agentHandle?.discovered?.contextWindowsByModel || {},
|
|
250
|
+
);
|
|
251
|
+
const built = buildGrokContextUsage({
|
|
252
|
+
usage: state.turnUsage,
|
|
253
|
+
model: usageModel,
|
|
254
|
+
contextWindow,
|
|
255
|
+
});
|
|
256
|
+
if (!built) return;
|
|
257
|
+
await api('POST', '/api/grok-context-usage', {
|
|
258
|
+
conversationId: message.conversationId,
|
|
259
|
+
sdkSessionId,
|
|
260
|
+
model: usageModel,
|
|
261
|
+
contextUsage: built.contextUsage,
|
|
262
|
+
modelUsage: built.modelUsage,
|
|
263
|
+
}).catch((error) => {
|
|
264
|
+
dbg('context usage publish failed', error?.message || String(error));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Ship per-turn token/cost totals for the Check Usage Grok card. Advisory:
|
|
270
|
+
* failures never disturb the turn reply.
|
|
271
|
+
*/
|
|
272
|
+
async function publishPlanUsage(message, state) {
|
|
273
|
+
if (!state.turnUsage) return;
|
|
274
|
+
await api('POST', '/api/grok-plan-usage', {
|
|
275
|
+
conversationId: message.conversationId,
|
|
276
|
+
sdkSessionId,
|
|
277
|
+
usage: state.turnUsage,
|
|
278
|
+
model: state.responseModel || state.turnUsage.modelId || '',
|
|
279
|
+
capturedAt: state.turnUsage.capturedAt || new Date().toISOString(),
|
|
280
|
+
}).catch((error) => {
|
|
281
|
+
dbg('plan usage publish failed', error?.message || String(error));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function publishFinalStream(message, text) {
|
|
286
|
+
await api('POST', '/api/stream', {
|
|
287
|
+
messageId: message.id,
|
|
288
|
+
conversationId: message.conversationId,
|
|
289
|
+
mode: message.relayMode || 'agent',
|
|
290
|
+
text: String(text || ''),
|
|
291
|
+
done: true,
|
|
292
|
+
}).catch(() => {});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function publishResponse(message, { text, model, terminalError = null, modelOrigin }) {
|
|
296
|
+
await api('POST', '/api/response', {
|
|
297
|
+
messageId: message.id,
|
|
298
|
+
conversationId: message.conversationId,
|
|
299
|
+
text: String(text || ''),
|
|
300
|
+
model: model || null,
|
|
301
|
+
modelOrigin: modelOrigin
|
|
302
|
+
|| (String(message?.model || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual'),
|
|
303
|
+
...(terminalError ? { terminalError } : {}),
|
|
304
|
+
}).catch(async () => {
|
|
305
|
+
await api('POST', '/api/requeue', { messageId: message.id }).catch(() => {});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function runTurn(message) {
|
|
310
|
+
currentAbortController = new AbortController();
|
|
311
|
+
const abortController = currentAbortController;
|
|
312
|
+
const state = {
|
|
313
|
+
result: null,
|
|
314
|
+
lastStreamedText: '',
|
|
315
|
+
responseModel: '',
|
|
316
|
+
turnUsage: null,
|
|
317
|
+
};
|
|
318
|
+
let aborted = false;
|
|
319
|
+
|
|
320
|
+
// The conversation's Grok model is pinned at bootstrap; the relay 409s a
|
|
321
|
+
// per-message switch before delivery, so the request here can only match
|
|
322
|
+
// the pinned model (or 'auto'). The handle keeps the model the session
|
|
323
|
+
// was created with — it is never relabeled to a merely requested one.
|
|
324
|
+
const requestedModel = String(message.model || '').trim();
|
|
325
|
+
const perTurnModel = requestedModel && requestedModel.toLowerCase() !== 'auto'
|
|
326
|
+
? requestedModel
|
|
327
|
+
: '';
|
|
328
|
+
const model = perTurnModel
|
|
329
|
+
|| String(message.providerModel || '').trim()
|
|
330
|
+
|| defaultModel;
|
|
331
|
+
|
|
332
|
+
await ensureAgentHandle(message, model);
|
|
333
|
+
|
|
334
|
+
const modeNudge = grokModeNudge(message.relayMode, lastNudgedRelayMode);
|
|
335
|
+
const userText = buildGrokUserText(message);
|
|
336
|
+
// Advisory: a preview lookup that fails costs the guidance, never the turn.
|
|
337
|
+
const previewBlock = previewInstructionsSent
|
|
338
|
+
? ''
|
|
339
|
+
: await Promise.resolve()
|
|
340
|
+
.then(() => getPreviewInstructions?.())
|
|
341
|
+
.catch(() => '');
|
|
342
|
+
const promptText = [previewBlock, modeNudge, userText].filter(Boolean).join('\n\n');
|
|
343
|
+
const pendingNudgedRelayMode = String(message.relayMode || 'agent').trim().toLowerCase();
|
|
344
|
+
|
|
345
|
+
let turn = null;
|
|
346
|
+
let planBoardPosted = false;
|
|
347
|
+
const controlState = controlPoller?.start?.({
|
|
348
|
+
queueMessageId: message.id,
|
|
349
|
+
onAbortTurn: async () => {
|
|
350
|
+
aborted = true;
|
|
351
|
+
abortController.abort();
|
|
352
|
+
await turn?.cancel?.();
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
let busyRetries = 0;
|
|
358
|
+
let actionsDispatched = false;
|
|
359
|
+
while (true) {
|
|
360
|
+
try {
|
|
361
|
+
turn = startGrokTurnImpl({
|
|
362
|
+
handle: agentHandle,
|
|
363
|
+
text: promptText,
|
|
364
|
+
reasoningEffort: message.reasoningEffort || '',
|
|
365
|
+
abortSignal: abortController.signal,
|
|
366
|
+
// The user's max-turn-duration setting overrides the worker-local
|
|
367
|
+
// absolute cap: "No limit" (0) must not be silently trimmed to
|
|
368
|
+
// the 30-minute default. The inactivity watchdog stays — it
|
|
369
|
+
// detects a dead transport, not a long turn.
|
|
370
|
+
watchdog: turnCeilingMs !== null
|
|
371
|
+
? { maxTurnMs: turnCeilingMs > 0 ? turnCeilingMs : 0 }
|
|
372
|
+
: null,
|
|
373
|
+
dbg,
|
|
374
|
+
});
|
|
375
|
+
for await (const action of turn) {
|
|
376
|
+
actionsDispatched = true;
|
|
377
|
+
await dispatchAction(message, action, state);
|
|
378
|
+
}
|
|
379
|
+
lastNudgedRelayMode = pendingNudgedRelayMode;
|
|
380
|
+
if (previewBlock) previewInstructionsSent = true;
|
|
381
|
+
break;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
// A stale agent whose previous prompt never settled reports busy;
|
|
384
|
+
// one close-and-resume usually clears it — but only before anything
|
|
385
|
+
// streamed: replaying the whole prompt over already-dispatched
|
|
386
|
+
// frames would duplicate text the user has seen. A busy after
|
|
387
|
+
// dispatch (or a second busy) is terminal.
|
|
388
|
+
if (busyRetries === 0 && !actionsDispatched && classifyErrorImpl(error).isBusy) {
|
|
389
|
+
busyRetries += 1;
|
|
390
|
+
dbg('grok agent busy, recreating handle for one retry');
|
|
391
|
+
await dispose();
|
|
392
|
+
await ensureAgentHandle(message, model);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (aborted || abortController.signal.aborted) {
|
|
400
|
+
dbg('turn aborted', message.id);
|
|
401
|
+
await publishFinalStream(message, state.lastStreamedText);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
// A stalled turn keeps whatever streamed before the hang so the user
|
|
405
|
+
// sees the partial reply next to the stall system note.
|
|
406
|
+
if (classifyErrorImpl(error).isStalled && state.lastStreamedText) {
|
|
407
|
+
await publishFinalStream(message, state.lastStreamedText);
|
|
408
|
+
}
|
|
409
|
+
throw error;
|
|
410
|
+
} finally {
|
|
411
|
+
controlPoller?.stop?.(controlState);
|
|
412
|
+
// Publish on every exit path that may have captured usage (including
|
|
413
|
+
// abort after a partial result). Fire-and-forget: advisory data must
|
|
414
|
+
// not gate the reply below — the comment used to claim this while the
|
|
415
|
+
// awaits made it false.
|
|
416
|
+
void publishContextUsage(message, state, model).catch(() => {});
|
|
417
|
+
void publishPlanUsage(message, state).catch(() => {});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (aborted) {
|
|
421
|
+
await publishFinalStream(message, state.lastStreamedText);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const result = state.result;
|
|
426
|
+
const responseModel = state.responseModel || model || null;
|
|
427
|
+
if (!result) {
|
|
428
|
+
// The turn ended without a terminal result; surface what streamed, or
|
|
429
|
+
// requeue so a transient hiccup is retried instead of burning the
|
|
430
|
+
// queue entry on a user-visible failure (Claude/Cursor parity).
|
|
431
|
+
const fallbackText = String(state.lastStreamedText || '').trim();
|
|
432
|
+
if (fallbackText) {
|
|
433
|
+
await publishFinalStream(message, fallbackText);
|
|
434
|
+
await publishResponse(message, { text: fallbackText, model: responseModel });
|
|
435
|
+
} else {
|
|
436
|
+
await api('POST', '/api/requeue', { messageId: message.id }).catch(() => {});
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (result.isError) {
|
|
442
|
+
const classified = classifyErrorImpl(new Error(result.errorMessage || result.stopReason || 'turn error'));
|
|
443
|
+
const errorText = classified.isAuth
|
|
444
|
+
? `System note: the Grok agent could not authenticate (${classified.message})`
|
|
445
|
+
: String(state.lastStreamedText || result.text || '').trim();
|
|
446
|
+
await publishFinalStream(message, state.lastStreamedText);
|
|
447
|
+
await publishResponse(message, {
|
|
448
|
+
text: errorText,
|
|
449
|
+
model: responseModel,
|
|
450
|
+
terminalError: buildGrokTerminalError(classified, message),
|
|
451
|
+
});
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const finalText = String(result.text || state.lastStreamedText || '').trim();
|
|
456
|
+
if (
|
|
457
|
+
!planBoardPosted
|
|
458
|
+
&& String(message.relayMode || '').trim().toLowerCase() === 'plan'
|
|
459
|
+
&& countPlanLikeLines(finalText) >= 2
|
|
460
|
+
) {
|
|
461
|
+
const boardPayload = buildGrokPlanReadyBoardPayload({ message, planText: finalText });
|
|
462
|
+
if (boardPayload) {
|
|
463
|
+
await api('POST', '/api/relay-board', boardPayload).catch(() => {});
|
|
464
|
+
planBoardPosted = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// A terminal, non-error result carrying no prose is a COMPLETED turn, not
|
|
468
|
+
// a failed delivery: requeueing re-billed a deterministically-empty turn
|
|
469
|
+
// on every retry until the cap failed it (Claude/Cursor parity via the
|
|
470
|
+
// shared note).
|
|
471
|
+
const publishedText = finalText || EMPTY_TURN_COMPLETION_NOTE;
|
|
472
|
+
await publishFinalStream(message, publishedText);
|
|
473
|
+
await publishResponse(message, { text: publishedText, model: responseModel });
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function handlePendingPayload(pending) {
|
|
478
|
+
const message = pending?.message || pending;
|
|
479
|
+
if (!message?.id) return false;
|
|
480
|
+
if (waitingForTurn) {
|
|
481
|
+
dbg('turn already active; rejecting concurrent deliver', message.id);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
waitingForTurn = true;
|
|
485
|
+
activeMessage = message;
|
|
486
|
+
try {
|
|
487
|
+
return await runTurn(message);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const classified = classifyErrorImpl(error);
|
|
490
|
+
dbg('turn failed', classified.code, classified.message);
|
|
491
|
+
// The guidance rides on the response text — there is no separate
|
|
492
|
+
// system-note channel (Claude/Cursor parity).
|
|
493
|
+
const noteText = classified.isAuth
|
|
494
|
+
? `System note: the Grok agent could not authenticate (${classified.message})`
|
|
495
|
+
: classified.isStalled
|
|
496
|
+
? `System note: the Grok turn stalled and was stopped (${classified.message}). Send a new message to retry.`
|
|
497
|
+
: `System note: the Grok turn failed (${classified.message}).`;
|
|
498
|
+
await publishResponse(message, {
|
|
499
|
+
text: noteText,
|
|
500
|
+
model: message.providerModel || defaultModel || null,
|
|
501
|
+
terminalError: buildGrokTerminalError(classified, message),
|
|
502
|
+
});
|
|
503
|
+
// Recreate handle after hard failures so the next turn is clean.
|
|
504
|
+
try { await dispose(); } catch { /* ignore */ }
|
|
505
|
+
return true;
|
|
506
|
+
} finally {
|
|
507
|
+
waitingForTurn = false;
|
|
508
|
+
activeMessage = null;
|
|
509
|
+
currentAbortController = null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
handlePendingPayload,
|
|
515
|
+
getActiveQueueMessageId,
|
|
516
|
+
isTurnActive,
|
|
517
|
+
dispose,
|
|
518
|
+
setTurnCeilingMs(value) {
|
|
519
|
+
const ms = Number(value);
|
|
520
|
+
turnCeilingMs = Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|