@oar-sh/oar 0.9.1 → 0.9.3
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/extension.mjs +0 -3
- package/.github/extensions/web-relay/polling/heartbeat.mjs +2 -55
- package/.github/extensions/web-relay/polling/polling-loop.mjs +0 -1
- package/.github/extensions/web-relay/runtime/api-client.mjs +2 -34
- package/.github/extensions/web-relay/runtime/config-loader.mjs +2 -104
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +2 -170
- package/.github/extensions/web-relay/runtime/session-io.mjs +1 -71
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +2 -325
- package/.github/extensions/web-relay/skills/prompt-context.mjs +2 -160
- package/CHANGELOG.md +269 -0
- package/README.md +407 -424
- package/bin/oar.js +142 -11
- package/package.json +8 -4
- package/server/README.md +59 -22
- package/server/claude-worker/claude-sdk-adapter.mjs +7 -2
- package/server/claude-worker/claude-session-process.mjs +1013 -45
- package/server/claude-worker/claude-session-worker.mjs +54 -9
- package/server/claude-worker/claude-turn-publisher.mjs +110 -19
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/copilot-sdk-runtime.mjs +12 -3
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-attachments.mjs +35 -2
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-model-switch.mjs +425 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +20 -8
- package/server/copilot-worker/copilot-question-bridge.mjs +129 -16
- package/server/copilot-worker/copilot-sdk-adapter.mjs +59 -11
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +136 -12
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2753 -527
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +94 -25
- package/server/cursor-worker/cursor-session-worker.mjs +5 -5
- package/server/cursor-worker/cursor-turn-runner.mjs +50 -4
- package/server/db-schema.mjs +61 -1
- package/server/features.mjs +155 -41
- package/server/grok-worker/grok-session-worker.mjs +5 -5
- package/server/grok-worker/grok-turn-runner.mjs +45 -4
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/public/app/annotate-core.mjs +181 -0
- package/server/public/app/annotate-editor.js +617 -0
- package/server/public/app/api-client.js +86 -5
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +172 -3
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +34 -19
- package/server/public/app/bootstrap.js +153 -69
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/context-tier-options.mjs +24 -4
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-preferences.mjs +47 -10
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +1032 -218
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/journal-view.js +190 -24
- package/server/public/app/model-selector-options.mjs +185 -1
- package/server/public/app/new-conversation-model-choice.mjs +29 -9
- package/server/public/app/pwa-install.js +64 -136
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/reasoning-effort-labels.mjs +13 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +26 -3
- package/server/public/app/router.js +120 -11
- package/server/public/app/settings-modal.js +225 -20
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +47 -4
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +159 -16
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/tool-activity-emoji.mjs +36 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/app/update-ui.js +194 -0
- package/server/public/index.html +483 -24
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +2 -0
- package/server/relay.mjs +23 -21
- package/server/repositories/message-repository.mjs +78 -21
- package/server/repositories/question-repository.mjs +19 -1
- package/server/repositories/session-repository.mjs +7 -31
- package/server/routes/ask-user-routes.mjs +10 -0
- package/server/routes/messages-routes.mjs +689 -159
- package/server/routes/sessions-routes.mjs +574 -80
- package/server/server-runtime.mjs +393 -925
- package/server/services/claude-auth-service.mjs +7 -1
- package/server/services/cli-install-service.mjs +8 -27
- package/server/services/cli-process-runner.mjs +30 -0
- package/server/services/copilot-model-discovery-service.mjs +202 -0
- package/server/services/data-dir-resolution.mjs +47 -0
- package/server/services/grok-auth-service.mjs +9 -1
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/model-variant-catalog-service.mjs +1183 -0
- package/server/services/oar-cli-helpers.mjs +6 -2
- package/server/services/relay-prompt-sanitizer.mjs +30 -3
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/sdk-session-import-service.mjs +144 -16
- package/server/services/sdk-session-sync-service.mjs +84 -27
- package/server/services/session-worker-registry-service.mjs +116 -3
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +119 -24
- package/server/services/status-event-service.mjs +8 -2
- package/server/services/steer-resend.mjs +45 -0
- package/server/services/update-check-service.mjs +220 -0
- package/server/services/update-install-service.mjs +279 -0
- package/server/services/windows-autostart-service.mjs +42 -6
- package/server/services/windows-boot-autostart-service.mjs +487 -0
- package/shared/ask-user-bridge.mjs +37 -5
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/claude-model-catalog.mjs +49 -0
- package/shared/context-window-fallbacks.mjs +1 -0
- package/shared/control-poller.mjs +4 -1
- package/shared/copilot-model-order.mjs +182 -0
- package/shared/media-embed-instructions.mjs +35 -0
- package/shared/model-descriptors.mjs +181 -30
- package/shared/model-id.mjs +6 -0
- package/shared/pwa-app-name.mjs +44 -0
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- package/shared/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/update-semver.mjs +50 -0
- package/shared/worker-crash-guard.mjs +40 -14
- package/shared/worker-runtime/api-client.mjs +34 -0
- package/shared/worker-runtime/config-loader.mjs +104 -0
- package/shared/worker-runtime/heartbeat.mjs +109 -0
- package/shared/worker-runtime/prompt-context.mjs +160 -0
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +170 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +436 -0
- package/server/claude-worker/claude-session-test-harness.mjs +0 -236
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -329
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.sh +0 -62
- package/server/services/fixtures/cli-install-stub.sh +0 -75
- package/server/services/fixtures/grok-stub.sh +0 -86
|
@@ -423,7 +423,6 @@ async function ensureSessionForConversation(conversationId, reason = "dequeue")
|
|
|
423
423
|
|
|
424
424
|
const sessionIo = createSessionIoHelpers({
|
|
425
425
|
getSession: () => session,
|
|
426
|
-
sleep,
|
|
427
426
|
dbg,
|
|
428
427
|
});
|
|
429
428
|
|
|
@@ -510,7 +509,6 @@ async function startPolling() {
|
|
|
510
509
|
setModelForMessage,
|
|
511
510
|
buildPromptWithRelayContext,
|
|
512
511
|
sendAndWaitWithHardTimeout,
|
|
513
|
-
sendWithBestEffortStreaming,
|
|
514
512
|
extractFinalText,
|
|
515
513
|
extractGeneratedImages,
|
|
516
514
|
getLastActivityText: () => getScopedLastActivityText(),
|
|
@@ -969,4 +967,3 @@ async function ensureRelayActive(reason) {
|
|
|
969
967
|
const extractFinalText = sessionIo.extractFinalText;
|
|
970
968
|
const extractGeneratedImages = sessionIo.extractGeneratedImages;
|
|
971
969
|
const sendAndWaitWithHardTimeout = sessionIo.sendAndWaitWithHardTimeout;
|
|
972
|
-
const sendWithBestEffortStreaming = sessionIo.sendWithBestEffortStreaming;
|
|
@@ -1,55 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
pollMs,
|
|
4
|
-
getSessionReady,
|
|
5
|
-
getHeartbeatTimer,
|
|
6
|
-
setHeartbeatTimer,
|
|
7
|
-
getActiveQueueMessageId,
|
|
8
|
-
// Optional: every queue message the worker currently owns work for (a
|
|
9
|
-
// persistent-process worker can hold a running turn, a delivered message
|
|
10
|
-
// queued behind it, and a background continuation at once). Each reported
|
|
11
|
-
// id gets its owner lease refreshed; unreported owned rows are recovered.
|
|
12
|
-
getActiveQueueMessageIds,
|
|
13
|
-
}) {
|
|
14
|
-
async function pulseHeartbeat() {
|
|
15
|
-
if (!getSessionReady()) return false;
|
|
16
|
-
try {
|
|
17
|
-
const activeQueueMessageId = typeof getActiveQueueMessageId === "function"
|
|
18
|
-
? String(getActiveQueueMessageId() || "").trim()
|
|
19
|
-
: "";
|
|
20
|
-
const activeQueueMessageIds = typeof getActiveQueueMessageIds === "function"
|
|
21
|
-
? (getActiveQueueMessageIds() || []).map((id) => String(id || "").trim()).filter(Boolean)
|
|
22
|
-
: [];
|
|
23
|
-
const body = {
|
|
24
|
-
...(activeQueueMessageId ? { activeQueueMessageId } : {}),
|
|
25
|
-
...(activeQueueMessageIds.length ? { activeQueueMessageIds } : {}),
|
|
26
|
-
};
|
|
27
|
-
await api("POST", "/api/heartbeat", body);
|
|
28
|
-
return true;
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function startHeartbeat() {
|
|
35
|
-
if (getHeartbeatTimer()) return;
|
|
36
|
-
void pulseHeartbeat();
|
|
37
|
-
const timer = setInterval(() => {
|
|
38
|
-
void pulseHeartbeat();
|
|
39
|
-
}, pollMs);
|
|
40
|
-
setHeartbeatTimer(timer);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function stopHeartbeat() {
|
|
44
|
-
const timer = getHeartbeatTimer();
|
|
45
|
-
if (!timer) return;
|
|
46
|
-
clearInterval(timer);
|
|
47
|
-
setHeartbeatTimer(null);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
pulseHeartbeat,
|
|
52
|
-
startHeartbeat,
|
|
53
|
-
stopHeartbeat,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
1
|
+
// relocated for extension retirement; shim dies with the extension engine
|
|
2
|
+
export * from "../../../../shared/worker-runtime/heartbeat.mjs";
|
|
@@ -1,34 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const url = `${serverUrl}${routePath}`;
|
|
4
|
-
const extraHeaders = typeof getHeaders === "function" ? (getHeaders() || {}) : {};
|
|
5
|
-
const opts = {
|
|
6
|
-
method,
|
|
7
|
-
headers: {
|
|
8
|
-
"Content-Type": "application/json",
|
|
9
|
-
Authorization: `Bearer ${token}`,
|
|
10
|
-
...extraHeaders,
|
|
11
|
-
},
|
|
12
|
-
...(method !== "GET" ? { body: JSON.stringify(body || {}) } : {}),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const res = await fetch(url, opts);
|
|
16
|
-
if (!res.ok) {
|
|
17
|
-
const rawText = String(await res.text().catch(() => "")).trim();
|
|
18
|
-
let detail = rawText;
|
|
19
|
-
if (rawText) {
|
|
20
|
-
try {
|
|
21
|
-
const payload = JSON.parse(rawText);
|
|
22
|
-
detail = String(payload?.error || payload?.message || rawText).trim();
|
|
23
|
-
} catch {
|
|
24
|
-
detail = rawText;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const error = new Error(`HTTP ${res.status} ${routePath}${detail ? `: ${detail}` : ""}`);
|
|
28
|
-
error.status = res.status;
|
|
29
|
-
error.detail = detail;
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
return res.json();
|
|
33
|
-
};
|
|
34
|
-
}
|
|
1
|
+
// relocated for extension retirement; shim dies with the extension engine
|
|
2
|
+
export * from "../../../../shared/worker-runtime/api-client.mjs";
|
|
@@ -1,104 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
function toAbsolutePath(value) {
|
|
6
|
-
if (!value) return "";
|
|
7
|
-
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function looksLikeServerDir(serverDir) {
|
|
11
|
-
if (!serverDir) return false;
|
|
12
|
-
return fs.existsSync(path.join(serverDir, "server.js")) && fs.existsSync(path.join(serverDir, "config.json"));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function resolveRelayPaths(importMetaUrl) {
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(importMetaUrl));
|
|
17
|
-
const projectServerDir = path.resolve(__dirname, "../../../server");
|
|
18
|
-
const cwdServerDir = path.resolve(process.cwd(), "server");
|
|
19
|
-
const envRoot = toAbsolutePath(process.env.COPILOT_WEB_RELAY_ROOT || "");
|
|
20
|
-
const envServerDir = toAbsolutePath(process.env.COPILOT_WEB_RELAY_SERVER_DIR || "");
|
|
21
|
-
const envConfigPath = toAbsolutePath(process.env.COPILOT_WEB_RELAY_CONFIG || "");
|
|
22
|
-
const envToolsPath = toAbsolutePath(process.env.COPILOT_WEB_RELAY_TOOLS || "");
|
|
23
|
-
const envLogDir = toAbsolutePath(process.env.COPILOT_WEB_RELAY_LOG_DIR || "");
|
|
24
|
-
|
|
25
|
-
const resolvedServerDir =
|
|
26
|
-
(looksLikeServerDir(envServerDir) && envServerDir) ||
|
|
27
|
-
(looksLikeServerDir(path.join(envRoot, "server")) && path.join(envRoot, "server")) ||
|
|
28
|
-
(looksLikeServerDir(projectServerDir) && projectServerDir) ||
|
|
29
|
-
(looksLikeServerDir(cwdServerDir) && cwdServerDir) ||
|
|
30
|
-
projectServerDir;
|
|
31
|
-
|
|
32
|
-
const SERVER_DIR = resolvedServerDir;
|
|
33
|
-
const CONFIG_PATH = envConfigPath || path.resolve(SERVER_DIR, "config.json");
|
|
34
|
-
const RELAY_TOOLS_PATH = envToolsPath || path.resolve(SERVER_DIR, "relay-tools.md");
|
|
35
|
-
const LOG_DIR = envLogDir || path.resolve(SERVER_DIR, "logs");
|
|
36
|
-
const SERVER_LOG_PATH = path.resolve(LOG_DIR, "server.log");
|
|
37
|
-
const SERVER_ERR_PATH = path.resolve(LOG_DIR, "server-err.log");
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
__dirname,
|
|
41
|
-
CONFIG_PATH,
|
|
42
|
-
RELAY_TOOLS_PATH,
|
|
43
|
-
SERVER_DIR,
|
|
44
|
-
LOG_DIR,
|
|
45
|
-
SERVER_LOG_PATH,
|
|
46
|
-
SERVER_ERR_PATH,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function loadTokenFromConfig(configPath) {
|
|
51
|
-
try {
|
|
52
|
-
const cfg = loadRelayConfig(configPath);
|
|
53
|
-
return String(cfg.authToken || "").trim();
|
|
54
|
-
} catch {
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function loadRelayConfig(configPath) {
|
|
60
|
-
try {
|
|
61
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
62
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
63
|
-
} catch {
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normalizeLoopbackHost(value) {
|
|
69
|
-
const host = String(value || "").trim();
|
|
70
|
-
if (!host || host === "0.0.0.0" || host === "::") return "127.0.0.1";
|
|
71
|
-
if (host === "localhost") return "127.0.0.1";
|
|
72
|
-
return host;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function resolveRelayServerUrl({ configPath, env = process.env } = {}) {
|
|
76
|
-
const explicitUrl = String(env.COPILOT_WEB_RELAY_SERVER_URL || "").trim();
|
|
77
|
-
if (explicitUrl) {
|
|
78
|
-
const url = new URL(explicitUrl);
|
|
79
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
80
|
-
throw new Error(`invalid-relay-server-url:${url.protocol}`);
|
|
81
|
-
}
|
|
82
|
-
url.pathname = "";
|
|
83
|
-
url.search = "";
|
|
84
|
-
url.hash = "";
|
|
85
|
-
return url.toString().replace(/\/$/, "");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const config = loadRelayConfig(configPath);
|
|
89
|
-
const configuredPort = Number.parseInt(String(config.port || ""), 10);
|
|
90
|
-
const port = Number.isInteger(configuredPort) && configuredPort > 0 && configuredPort <= 65535
|
|
91
|
-
? configuredPort
|
|
92
|
-
: 3333;
|
|
93
|
-
const host = normalizeLoopbackHost(config.host);
|
|
94
|
-
const formattedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
95
|
-
return `http://${formattedHost}:${port}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function loadRelayInstructionsFromFile(relayToolsPath) {
|
|
99
|
-
try {
|
|
100
|
-
return String(fs.readFileSync(relayToolsPath, "utf8")).trim();
|
|
101
|
-
} catch {
|
|
102
|
-
return "";
|
|
103
|
-
}
|
|
104
|
-
}
|
|
1
|
+
// relocated for extension retirement; shim dies with the extension engine
|
|
2
|
+
export * from "../../../../shared/worker-runtime/config-loader.mjs";
|
|
@@ -1,170 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function extractFirstMatch(text, pattern) {
|
|
6
|
-
const match = text.match(pattern);
|
|
7
|
-
return String(match?.[1] || "").trim() || null;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function toKebabToken(value) {
|
|
11
|
-
return String(value || "")
|
|
12
|
-
.trim()
|
|
13
|
-
.toLowerCase()
|
|
14
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
15
|
-
.replace(/^-+|-+$/g, "") || null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// The runtime's own vocabulary for "the billing window is spent". The Copilot
|
|
19
|
-
// SDK's `ErrorData.errorCode` documents `quota_exceeded` and
|
|
20
|
-
// `session_quota_exceeded`; `errorType` documents the coarser `quota`
|
|
21
|
-
// category; HTTP 402 is the transport-level form of the same thing.
|
|
22
|
-
const QUOTA_ERROR_CODES = new Set([
|
|
23
|
-
"quota_exceeded",
|
|
24
|
-
"session_quota_exceeded",
|
|
25
|
-
"quota-exceeded",
|
|
26
|
-
"session-quota-exceeded",
|
|
27
|
-
"quota",
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* True when a runtime flagged quota exhaustion STRUCTURALLY, independent of
|
|
32
|
-
* how the prose was worded. This exists because the prose is the one part
|
|
33
|
-
* GitHub rewords freely ("premium requests" → "AI credits" → "monthly quota"),
|
|
34
|
-
* and a quota failure misfiled as retryable burns the retry budget against a
|
|
35
|
-
* window that cannot reset until the next billing period.
|
|
36
|
-
*
|
|
37
|
-
* Accepts the loose shapes callers actually hold: an `Error` with `code` /
|
|
38
|
-
* `statusCode`, or a runtime error payload with `errorCode` / `errorType` /
|
|
39
|
-
* `statusCode`.
|
|
40
|
-
*/
|
|
41
|
-
export function isStructuredQuotaError(source) {
|
|
42
|
-
if (!source || typeof source !== "object") return false;
|
|
43
|
-
for (const field of ["code", "errorCode", "errorType"]) {
|
|
44
|
-
const token = String(source[field] || "").trim().toLowerCase();
|
|
45
|
-
if (token && QUOTA_ERROR_CODES.has(token)) return true;
|
|
46
|
-
}
|
|
47
|
-
const status = Number(source.statusCode ?? source.status);
|
|
48
|
-
return status === 402;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const QUOTA_EXHAUSTED = {
|
|
52
|
-
code: "quota-exhausted",
|
|
53
|
-
message: "GitHub Copilot has no AI credits left for this billing window, so the request was rejected.",
|
|
54
|
-
guidance: "Open Check Usage for the reset time and retry after the reset, or switch this conversation to another provider.",
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* `hint` carries structured signals that outrank the prose. Only quota is
|
|
59
|
-
* modelled today; the prose branches below are unchanged and remain the sole
|
|
60
|
-
* classifier when no hint is supplied, which is the extension's path.
|
|
61
|
-
*/
|
|
62
|
-
function classifyTerminalError(text, hint = {}) {
|
|
63
|
-
// Checked before the prose so a structurally-flagged quota error lands in
|
|
64
|
-
// the non-retryable branch however mildly it was worded.
|
|
65
|
-
if (hint.quota === true) return QUOTA_EXHAUSTED;
|
|
66
|
-
|
|
67
|
-
const lower = text.toLowerCase();
|
|
68
|
-
if (!lower) return null;
|
|
69
|
-
|
|
70
|
-
if (lower.includes("no tool output found for function call")) {
|
|
71
|
-
return {
|
|
72
|
-
code: "missing-tool-output",
|
|
73
|
-
message: "No tool output was returned for a required function call.",
|
|
74
|
-
guidance: "Retry the message. If this keeps happening, restart the relay and include the error code.",
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
if (lower.includes("tool call") && (lower.includes("not found") || lower.includes("missing"))) {
|
|
78
|
-
return {
|
|
79
|
-
code: "tool-call-missing",
|
|
80
|
-
message: "A required tool call was missing in the runtime response.",
|
|
81
|
-
guidance: "Retry the message. If it repeats, restart the relay and include the error code.",
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
if (lower.includes("tool output") && lower.includes("invalid")) {
|
|
85
|
-
return {
|
|
86
|
-
code: "invalid-tool-output",
|
|
87
|
-
message: "Tool output returned from the runtime was invalid.",
|
|
88
|
-
guidance: "Retry the message. If it repeats, restart the relay and include the error code.",
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
// Monthly/plan quota exhaustion is not transient: retrying before the
|
|
92
|
-
// billing window resets can never succeed. Distinct from "rate limit"
|
|
93
|
-
// (per-minute throttling), which stays retryable below.
|
|
94
|
-
if ((lower.includes("quota") && (lower.includes("exceeded") || lower.includes("exhaust")))
|
|
95
|
-
|| lower.includes("out of ai credits")
|
|
96
|
-
|| lower.includes("premium request allowance")) {
|
|
97
|
-
return QUOTA_EXHAUSTED;
|
|
98
|
-
}
|
|
99
|
-
if ((lower.includes("capierror") || lower.includes("http 400") || lower.includes("status 400") || /^400\b/.test(lower))
|
|
100
|
-
&& !lower.includes("timeout")
|
|
101
|
-
&& !lower.includes("temporar")
|
|
102
|
-
&& !lower.includes("rate limit")) {
|
|
103
|
-
return {
|
|
104
|
-
code: "request-invalid",
|
|
105
|
-
message: "The runtime rejected the request as invalid and non-retryable.",
|
|
106
|
-
guidance: "Retry after adjusting the request. If it persists, restart the relay and include the error code.",
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* `options` lets a caller that holds structured error fields (an SDK error
|
|
115
|
-
* payload rather than a thrown `Error`) pass them in explicitly:
|
|
116
|
-
* `{ quota: true }`, or `{ errorCode, errorType, statusCode }` to be judged
|
|
117
|
-
* here. Structured fields carried on the error object itself (`error.code`,
|
|
118
|
-
* `error.statusCode`) are read as well, so plain HTTP-ish errors classify
|
|
119
|
-
* without the caller doing anything.
|
|
120
|
-
*/
|
|
121
|
-
export function normalizeTerminalSendAndWaitError(error, options = {}) {
|
|
122
|
-
const detail = readErrorText(error);
|
|
123
|
-
const quota = options.quota === true
|
|
124
|
-
|| isStructuredQuotaError(options)
|
|
125
|
-
|| isStructuredQuotaError(error);
|
|
126
|
-
const base = classifyTerminalError(detail, { quota });
|
|
127
|
-
if (!base) return null;
|
|
128
|
-
const functionCallId = extractFirstMatch(detail, /function call\s+([a-z0-9_-]+)/i)
|
|
129
|
-
|| extractFirstMatch(detail, /\b(call_[a-z0-9_-]+)/i);
|
|
130
|
-
// GitHub request ids are colon-separated hex segments (AFAC:3C07CD:…).
|
|
131
|
-
const requestId = extractFirstMatch(detail, /\brequest(?:\s+id|_id)?[:=]\s*([a-z0-9_-]+(?::[a-z0-9_-]+)*)/i)
|
|
132
|
-
|| extractFirstMatch(detail, /\b(req_[a-z0-9_-]+)/i);
|
|
133
|
-
return {
|
|
134
|
-
terminal: true,
|
|
135
|
-
code: base.code,
|
|
136
|
-
stableCode: `relay.${base.code}`,
|
|
137
|
-
message: base.message,
|
|
138
|
-
guidance: base.guidance,
|
|
139
|
-
detail: detail || "unknown error",
|
|
140
|
-
functionCallId,
|
|
141
|
-
requestId,
|
|
142
|
-
classificationHint: toKebabToken(error?.code || error?.name || null),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function isTerminalSendAndWaitError(error, options = {}) {
|
|
147
|
-
return !!normalizeTerminalSendAndWaitError(error, options);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function buildTerminalFailureText(error, options = {}) {
|
|
151
|
-
const normalized = normalizeTerminalSendAndWaitError(error, options) || {
|
|
152
|
-
stableCode: "relay.unknown-terminal",
|
|
153
|
-
message: "The relay runtime hit a terminal error and could not complete this turn.",
|
|
154
|
-
guidance: "Retry the message.",
|
|
155
|
-
detail: readErrorText(error) || "unknown error",
|
|
156
|
-
functionCallId: null,
|
|
157
|
-
requestId: null,
|
|
158
|
-
};
|
|
159
|
-
const ids = [
|
|
160
|
-
normalized.functionCallId ? `functionCallId=${normalized.functionCallId}` : null,
|
|
161
|
-
normalized.requestId ? `requestId=${normalized.requestId}` : null,
|
|
162
|
-
].filter(Boolean);
|
|
163
|
-
return [
|
|
164
|
-
normalized.message,
|
|
165
|
-
`Error code: ${normalized.stableCode}.`,
|
|
166
|
-
ids.length ? `IDs: ${ids.join(", ")}.` : null,
|
|
167
|
-
normalized.guidance,
|
|
168
|
-
`Details: ${normalized.detail}`,
|
|
169
|
-
].filter(Boolean).join(" ");
|
|
170
|
-
}
|
|
1
|
+
// relocated for extension retirement; shim dies with the extension engine
|
|
2
|
+
export * from "../../../../shared/worker-runtime/send-and-wait-errors.mjs";
|
|
@@ -223,17 +223,7 @@ function extractGeneratedImages(finalEvent) {
|
|
|
223
223
|
return deduped.slice(0, 8);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
export function createSessionIoHelpers({ getSession,
|
|
227
|
-
const STREAM_WAIT_POLL_MS = 5_000;
|
|
228
|
-
const WAIT_TICK = Symbol("relay-stream-wait-tick");
|
|
229
|
-
|
|
230
|
-
function buildRelayStreamTimeoutError(timeoutMs) {
|
|
231
|
-
const error = new Error(`Hard timeout after ${timeoutMs}ms while streaming session.send`);
|
|
232
|
-
error.code = "RELAY_STREAM_TIMEOUT";
|
|
233
|
-
error.stableCode = "relay.stream-timeout";
|
|
234
|
-
return error;
|
|
235
|
-
}
|
|
236
|
-
|
|
226
|
+
export function createSessionIoHelpers({ getSession, dbg = () => {} }) {
|
|
237
227
|
async function sendAndWaitWithHardTimeout(payload, timeoutMs) {
|
|
238
228
|
// The loser timer is cancelled when the race settles: a fast turn must
|
|
239
229
|
// not leave a five-minute timer armed to fire a rejection into a settled
|
|
@@ -254,65 +244,6 @@ export function createSessionIoHelpers({ getSession, sleep, dbg = () => {} }) {
|
|
|
254
244
|
}
|
|
255
245
|
}
|
|
256
246
|
|
|
257
|
-
async function sendWithBestEffortStreaming(payload, timeoutMs, onEvent, options = {}) {
|
|
258
|
-
const session = getSession();
|
|
259
|
-
if (!session) throw new Error("No active Copilot session");
|
|
260
|
-
if (typeof session.send !== "function") {
|
|
261
|
-
return sendAndWaitWithHardTimeout(payload, timeoutMs);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const streamOrResult = await session.send(payload);
|
|
265
|
-
if (streamOrResult && typeof streamOrResult[Symbol.asyncIterator] === "function") {
|
|
266
|
-
const deadline = Date.now() + timeoutMs + 5_000;
|
|
267
|
-
let finalEvent = null;
|
|
268
|
-
const iterator = streamOrResult[Symbol.asyncIterator]();
|
|
269
|
-
const onWaiting = typeof options?.onWaiting === "function" ? options.onWaiting : null;
|
|
270
|
-
const waitPollMs = Math.max(250, Math.min(Number(options?.waitPollMs) || STREAM_WAIT_POLL_MS, 30_000));
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
while (true) {
|
|
274
|
-
const remainingMs = deadline - Date.now();
|
|
275
|
-
if (remainingMs <= 0) {
|
|
276
|
-
throw buildRelayStreamTimeoutError(timeoutMs);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const nextOrTick = await Promise.race([
|
|
280
|
-
iterator.next(),
|
|
281
|
-
sleep(Math.min(waitPollMs, remainingMs)).then(() => WAIT_TICK),
|
|
282
|
-
]);
|
|
283
|
-
|
|
284
|
-
if (nextOrTick === WAIT_TICK) {
|
|
285
|
-
if (onWaiting) {
|
|
286
|
-
await onWaiting({
|
|
287
|
-
timeoutMs,
|
|
288
|
-
deadline,
|
|
289
|
-
remainingMs: Math.max(0, deadline - Date.now()),
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (nextOrTick?.done) break;
|
|
296
|
-
const event = nextOrTick?.value;
|
|
297
|
-
finalEvent = event;
|
|
298
|
-
if (typeof onEvent === "function") {
|
|
299
|
-
try { await onEvent(event); } catch {}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} finally {
|
|
303
|
-
if (typeof iterator?.return === "function") {
|
|
304
|
-
try { await iterator.return(); } catch {}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return finalEvent || {};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (typeof onEvent === "function") {
|
|
311
|
-
try { await onEvent(streamOrResult); } catch {}
|
|
312
|
-
}
|
|
313
|
-
return streamOrResult;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
247
|
function extractFinalTextWithLogging(finalEvent) {
|
|
317
248
|
const result = extractFinalText(finalEvent, dbg);
|
|
318
249
|
// Always log envelope structure so we can diagnose extraction misses
|
|
@@ -344,6 +275,5 @@ export function createSessionIoHelpers({ getSession, sleep, dbg = () => {} }) {
|
|
|
344
275
|
extractFinalText: extractFinalTextWithLogging,
|
|
345
276
|
extractGeneratedImages: extractGeneratedImagesWithLogging,
|
|
346
277
|
sendAndWaitWithHardTimeout,
|
|
347
|
-
sendWithBestEffortStreaming,
|
|
348
278
|
};
|
|
349
279
|
}
|