@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyPreviewInstructions,
|
|
3
|
+
createPreviewInstructionsProvider,
|
|
4
|
+
} from "../preview-instructions.mjs";
|
|
5
|
+
|
|
6
|
+
export { createPreviewInstructionsProvider };
|
|
7
|
+
|
|
8
|
+
export function buildPrompt(message) {
|
|
9
|
+
const text = String(message?.text || "").trim();
|
|
10
|
+
const attachments = Array.isArray(message?.attachments) ? message.attachments : [];
|
|
11
|
+
if (!attachments.length) return text;
|
|
12
|
+
|
|
13
|
+
const attachmentNote = attachments.map((att) => {
|
|
14
|
+
const name = att?.name ? String(att.name) : "attachment";
|
|
15
|
+
const type = att?.type ? String(att.type) : "file";
|
|
16
|
+
if (type.startsWith("image/")) return `${name} (${type}, inline image attachment)`;
|
|
17
|
+
const reference = att?.path ? ` @${att.path}` : "";
|
|
18
|
+
return `${name} (${type})${reference}`;
|
|
19
|
+
}).join(", ");
|
|
20
|
+
const suffix = `\n\n[Attached file${attachments.length === 1 ? "" : "s"}: ${attachmentNote}]`;
|
|
21
|
+
return text ? `${text}${suffix}` : suffix.trimStart();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeRelayMode(mode) {
|
|
25
|
+
const value = String(mode || "agent").trim().toLowerCase();
|
|
26
|
+
if (value === "plan" || value === "ask" || value === "autopilot" || value === "agent") return value;
|
|
27
|
+
return "agent";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildModeInstructionText(mode) {
|
|
31
|
+
if (mode === "plan") {
|
|
32
|
+
return [
|
|
33
|
+
"Draft a concise plan only.",
|
|
34
|
+
"Use read-only inspection tools (glob, rg, view) only when they materially improve plan quality; otherwise draft from provided context.",
|
|
35
|
+
"Do not edit repository files or run mutating commands unless the user explicitly asks for implementation.",
|
|
36
|
+
"If clarification is required, pause and ask through the web relay.",
|
|
37
|
+
"These instructions remain in effect until relay mode changes.",
|
|
38
|
+
].join(" ");
|
|
39
|
+
}
|
|
40
|
+
if (mode === "ask") {
|
|
41
|
+
return [
|
|
42
|
+
"Prioritize clarification questions before doing any implementation work.",
|
|
43
|
+
"If the request is ambiguous or underspecified, pause and ask through the web relay before making assumptions.",
|
|
44
|
+
"Do not make broad assumptions when a question would materially change the result.",
|
|
45
|
+
"These instructions remain in effect until relay mode changes.",
|
|
46
|
+
].join(" ");
|
|
47
|
+
}
|
|
48
|
+
if (mode === "autopilot") {
|
|
49
|
+
return [
|
|
50
|
+
"Act directly on the request and use tools when needed.",
|
|
51
|
+
"Keep moving unless user input is truly blocking.",
|
|
52
|
+
"These instructions remain in effect until relay mode changes.",
|
|
53
|
+
].join(" ");
|
|
54
|
+
}
|
|
55
|
+
return [
|
|
56
|
+
"Proceed as an interactive coding agent and use tools as needed.",
|
|
57
|
+
"If you need clarification, pause and ask through the web relay instead of stalling silently.",
|
|
58
|
+
"These instructions remain in effect until relay mode changes.",
|
|
59
|
+
].join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildModeMarker(mode) {
|
|
63
|
+
return `[Relay mode: ${normalizeRelayMode(mode)}]`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildModePrompt(mode, toolInstructions = "", options = {}) {
|
|
67
|
+
const normalizedMode = normalizeRelayMode(mode);
|
|
68
|
+
const includeInstructions = options?.includeInstructions !== false;
|
|
69
|
+
const parts = [buildModeMarker(normalizedMode)];
|
|
70
|
+
if (includeInstructions) {
|
|
71
|
+
parts.push(buildModeInstructionText(normalizedMode));
|
|
72
|
+
if (toolInstructions) {
|
|
73
|
+
parts.push(toolInstructions);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return parts.join(" ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildPromptWithMode(message, toolInstructions = "", options = {}) {
|
|
80
|
+
const modePrompt = buildModePrompt(message?.relayMode, toolInstructions, options);
|
|
81
|
+
const body = buildPrompt(message);
|
|
82
|
+
return [modePrompt, body].filter(Boolean).join(" ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The relay's prompt prefix builder: tool guidance rides along only when the
|
|
87
|
+
* relay mode changed, so repeat turns in one mode stay cheap.
|
|
88
|
+
*/
|
|
89
|
+
export function createRelayPromptBuilder({
|
|
90
|
+
toolInstructions = "",
|
|
91
|
+
getPreviewInstructions = null,
|
|
92
|
+
} = {}) {
|
|
93
|
+
let lastPromptedRelayMode = null;
|
|
94
|
+
return async function buildPromptWithRelayContext(message) {
|
|
95
|
+
const relayMode = normalizeRelayMode(message?.relayMode);
|
|
96
|
+
const includeInstructions = lastPromptedRelayMode !== relayMode;
|
|
97
|
+
let instructions = "";
|
|
98
|
+
if (includeInstructions) {
|
|
99
|
+
// Guidance is advisory: a preview lookup that fails must cost the block,
|
|
100
|
+
// never the turn.
|
|
101
|
+
const previewBlock = typeof getPreviewInstructions === "function"
|
|
102
|
+
? await Promise.resolve().then(getPreviewInstructions).catch(() => "")
|
|
103
|
+
: "";
|
|
104
|
+
instructions = applyPreviewInstructions(toolInstructions, previewBlock);
|
|
105
|
+
}
|
|
106
|
+
const prompt = buildPromptWithMode({ ...message, relayMode }, instructions, { includeInstructions });
|
|
107
|
+
lastPromptedRelayMode = relayMode;
|
|
108
|
+
return prompt;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function stripPromptPrefix(text, promptPrefix = "") {
|
|
113
|
+
const value = String(text || "").trim();
|
|
114
|
+
if (!value) return "";
|
|
115
|
+
const prompt = String(promptPrefix || "").trim();
|
|
116
|
+
if (!prompt) return value;
|
|
117
|
+
if (!value.startsWith(prompt)) return value;
|
|
118
|
+
const remainder = value.slice(prompt.length).trim();
|
|
119
|
+
return remainder || "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function escapeRegExp(value) {
|
|
123
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function whitespaceFlexiblePattern(value) {
|
|
127
|
+
return String(value || "")
|
|
128
|
+
.trim()
|
|
129
|
+
.split(/\s+/)
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map((part) => escapeRegExp(part))
|
|
132
|
+
.join("\\s+");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stripPromptPrefixFlexible(text, promptPrefix = "") {
|
|
136
|
+
const value = String(text || "").trim();
|
|
137
|
+
const prompt = String(promptPrefix || "").trim();
|
|
138
|
+
if (!value || !prompt) return value;
|
|
139
|
+
const pattern = new RegExp(`^${whitespaceFlexiblePattern(prompt)}\\s*`, "i");
|
|
140
|
+
const stripped = value.replace(pattern, "").trim();
|
|
141
|
+
return stripped || "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function stripPromptContextPrefix(text, message, toolInstructions = "", promptPrefix = "") {
|
|
145
|
+
const value = String(text || "").trim();
|
|
146
|
+
if (!value) return "";
|
|
147
|
+
const candidates = [
|
|
148
|
+
String(promptPrefix || "").trim(),
|
|
149
|
+
buildPromptWithMode(message, toolInstructions),
|
|
150
|
+
buildPromptWithMode(message, toolInstructions, { includeInstructions: false }),
|
|
151
|
+
buildModeMarker(message?.relayMode),
|
|
152
|
+
].filter(Boolean);
|
|
153
|
+
for (const candidate of candidates) {
|
|
154
|
+
const stripped = stripPromptPrefix(value, candidate);
|
|
155
|
+
if (stripped !== value) return stripped;
|
|
156
|
+
const flexibleStripped = stripPromptPrefixFlexible(value, candidate);
|
|
157
|
+
if (flexibleStripped !== value) return flexibleStripped;
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Glue between a steering-capable session runner and the relay socket link —
|
|
2
|
+
// shared by every worker whose runner exposes a delivery gate (the Claude
|
|
3
|
+
// session runner, the Copilot SDK runner). Kept out of the worker entry files
|
|
4
|
+
// (which run main() on import) so the wiring itself is testable.
|
|
5
|
+
|
|
6
|
+
import { requeueOwedRows } from '../worker-crash-guard.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The runner reports every flip of its delivery gate; the link turns them
|
|
10
|
+
* into relay readiness frames. `attach` is late-bound because the runner is
|
|
11
|
+
* built before the link (the link's probes call into the runner).
|
|
12
|
+
*/
|
|
13
|
+
export function createRunnerLinkBridge() {
|
|
14
|
+
let link = null;
|
|
15
|
+
return {
|
|
16
|
+
attach(nextLink) {
|
|
17
|
+
link = nextLink || null;
|
|
18
|
+
},
|
|
19
|
+
onDeliveryReadinessChange(ready) {
|
|
20
|
+
if (!link) return;
|
|
21
|
+
if (ready) void link.notifyReady('steering-resumed');
|
|
22
|
+
else link.notifyUnready('steering-held');
|
|
23
|
+
},
|
|
24
|
+
// Only a relay that advertised the capability in server.hello understands
|
|
25
|
+
// the penalty-free hand-back; before any hello, assume an older relay.
|
|
26
|
+
canHandBackHeldDelivery() {
|
|
27
|
+
return Boolean(link?.serverSupports?.('steering-held'));
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Worker shutdown (SIGTERM/SIGINT): rows whose prompt the engine already
|
|
34
|
+
* consumed and whose settle has not landed are failed terminally before the
|
|
35
|
+
* process goes — left behind, the relay's dead-worker recovery would requeue
|
|
36
|
+
* them and run them a second time. Bounded like the crash guard: a shutdown
|
|
37
|
+
* that hangs is worse than none. Other owed rows are left to the relay's
|
|
38
|
+
* recovery, as before.
|
|
39
|
+
*/
|
|
40
|
+
export async function failSettlingRowsOnShutdown({ api, runner, timeoutMs = 2_000 } = {}) {
|
|
41
|
+
const settling = (runner?.getActiveQueueMessageIds?.() || []).filter((entry) => entry?.terminalError);
|
|
42
|
+
await requeueOwedRows({ api, entries: settling, timeoutMs });
|
|
43
|
+
return settling.length;
|
|
44
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
function readErrorText(error) {
|
|
2
|
+
return String(error?.message || error || "").trim();
|
|
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
|
+
}
|