@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0-preview.20260912
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/gui/dist/assets/index-D_t6sCWs.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin / Cognition / Windsurf adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses the unofficial cloud-direct Connect-RPC client (GetChatMessage).
|
|
5
|
+
* OpenCodex injects the OAuth API key onto provider.apiKey
|
|
6
|
+
* before runTurn. This adapter maps OcxContext <-> ChatHistoryItem and
|
|
7
|
+
* streams CloudChatEvent into AdapterEvent.
|
|
8
|
+
*/
|
|
9
|
+
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxToolCall, OcxToolResultMessage, OcxUsage } from "../types";
|
|
10
|
+
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
11
|
+
import { streamChatEvents, allocateCascadeId, CloudChatError, type ChatHistoryItem, type ToolDef } from "./devin/cloud-direct";
|
|
12
|
+
import { getCachedCatalog } from "./devin/cloud-direct/catalog";
|
|
13
|
+
import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin";
|
|
14
|
+
|
|
15
|
+
export const DEVIN_API_SERVER = DEVIN_DEFAULT_API_SERVER;
|
|
16
|
+
|
|
17
|
+
const EFFORT_SUFFIXES = new Set(["low", "medium", "high", "xhigh", "max", "none", "1m", "max-1m", "none-1m", "fast"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cognition's catalog spells model ids with hyphens (`swe-1-7`), but the same
|
|
21
|
+
* models appear elsewhere - other proxies, hand-written config - with the dotted
|
|
22
|
+
* version number (`swe-1.7`). Left alone, a dotted id misses every catalog
|
|
23
|
+
* lookup and then gets an effort suffix appended to a name the server does not
|
|
24
|
+
* know, which Cognition answers with an opaque permission_denied.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeDevinModelId(modelId: string): string {
|
|
27
|
+
return modelId.replace(/\./g, "-");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasEffortSuffix(modelId: string): boolean {
|
|
31
|
+
const parts = modelId.split("-");
|
|
32
|
+
return parts.length > 1 && EFFORT_SUFFIXES.has(parts[parts.length - 1]!);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the wire model UID using the live catalog as the source of truth.
|
|
37
|
+
* Cognition's catalog lists most models with an effort suffix
|
|
38
|
+
* (e.g. `gpt-5-6-sol-high`); the base id alone is not accepted for those.
|
|
39
|
+
*
|
|
40
|
+
* If the catalog is available: use the exact UID when it exists, otherwise
|
|
41
|
+
* append the reasoning effort (or `medium` default) and pick a variant the
|
|
42
|
+
* account actually has.
|
|
43
|
+
*
|
|
44
|
+
* If the catalog is unavailable (degraded mode): append the effort suffix
|
|
45
|
+
* for any base id that doesn't already carry one, mirroring the catalog shape.
|
|
46
|
+
*/
|
|
47
|
+
async function resolveWireModelUid(
|
|
48
|
+
rawModelId: string,
|
|
49
|
+
apiKey: string,
|
|
50
|
+
host: string,
|
|
51
|
+
reasoningEffort?: string,
|
|
52
|
+
): Promise<string> {
|
|
53
|
+
const modelId = normalizeDevinModelId(rawModelId);
|
|
54
|
+
if (hasEffortSuffix(modelId)) return modelId;
|
|
55
|
+
const catalog = await getCachedCatalog(apiKey, host);
|
|
56
|
+
if (catalog) {
|
|
57
|
+
if (catalog.byUid.has(modelId)) return modelId;
|
|
58
|
+
const effort = reasoningEffort && EFFORT_SUFFIXES.has(reasoningEffort) ? reasoningEffort : "medium";
|
|
59
|
+
const suffixed = `${modelId}-${effort}`;
|
|
60
|
+
if (catalog.byUid.has(suffixed)) return suffixed;
|
|
61
|
+
// Fall back to any enabled variant of this base model.
|
|
62
|
+
for (const uid of catalog.byUid.keys()) {
|
|
63
|
+
if (uid.startsWith(modelId + "-") && !catalog.byUid.get(uid)?.disabled) return uid;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Degraded mode: append the default effort suffix.
|
|
67
|
+
const effort = reasoningEffort && EFFORT_SUFFIXES.has(reasoningEffort) ? reasoningEffort : "medium";
|
|
68
|
+
return `${modelId}-${effort}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class DevinMissingCredentialError extends Error {
|
|
72
|
+
constructor() {
|
|
73
|
+
super("Devin live transport requires a Devin API key. Run ocx login devin to sign in with your Cognition/Devin account.");
|
|
74
|
+
this.name = "DevinMissingCredentialError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveDevinToken(provider: OcxProviderConfig, headers?: Headers): string {
|
|
79
|
+
const providerKey = provider.apiKey?.trim();
|
|
80
|
+
if (providerKey) return providerKey;
|
|
81
|
+
const forwarded = headers?.get("authorization") ?? headers?.get("Authorization");
|
|
82
|
+
if (forwarded?.toLowerCase().startsWith("bearer ")) return forwarded.slice("bearer ".length).trim();
|
|
83
|
+
const envToken = process.env.OPENCODEX_DEVIN_TEST_TOKEN?.trim();
|
|
84
|
+
if (envToken) return envToken;
|
|
85
|
+
throw new DevinMissingCredentialError();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function textFromParts(content: string | OcxContentPart[] | undefined): string {
|
|
89
|
+
if (typeof content === "string") return content;
|
|
90
|
+
if (!Array.isArray(content)) return "";
|
|
91
|
+
return content.map((part) => (part.type === "text" ? part.text : "")).filter(Boolean).join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toolResultText(message: OcxToolResultMessage): string {
|
|
95
|
+
const body = textFromParts(message.content);
|
|
96
|
+
return message.isError ? ("ERROR: " + body) : body;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function assistantToolCalls(message: OcxAssistantMessage): Array<{ id: string; name: string; arguments: string }> {
|
|
100
|
+
return message.content
|
|
101
|
+
.filter((part): part is OcxToolCall => part.type === "toolCall")
|
|
102
|
+
.map((part) => ({
|
|
103
|
+
id: part.id,
|
|
104
|
+
name: part.name,
|
|
105
|
+
arguments: JSON.stringify(part.arguments ?? {}),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assistantText(message: OcxAssistantMessage): string {
|
|
110
|
+
return message.content
|
|
111
|
+
// Thinking stays out of the replayed content. Cognition has no reasoning
|
|
112
|
+
// replay field, and folding chain-of-thought into assistant text sends it
|
|
113
|
+
// back as visible prior output - which the model then treats as something
|
|
114
|
+
// it said to the user.
|
|
115
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mapOcxMessagesToDevin(parsed: OcxParsedRequest): ChatHistoryItem[] {
|
|
121
|
+
const items: ChatHistoryItem[] = [];
|
|
122
|
+
const system = parsed.context.systemPrompt?.filter((line) => line.trim().length > 0).join("\n");
|
|
123
|
+
if (system) items.push({ role: "system", content: system });
|
|
124
|
+
|
|
125
|
+
for (const message of parsed.context.messages) {
|
|
126
|
+
const mapped = mapOneMessage(message);
|
|
127
|
+
if (mapped) items.push(mapped);
|
|
128
|
+
}
|
|
129
|
+
return items;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
|
|
133
|
+
if (message.role === "user" || message.role === "developer") {
|
|
134
|
+
const text = textFromParts(message.content).trim();
|
|
135
|
+
if (!text) return undefined;
|
|
136
|
+
return { role: message.role === "developer" ? "system" : "user", content: text };
|
|
137
|
+
}
|
|
138
|
+
if (message.role === "assistant") {
|
|
139
|
+
const toolCalls = assistantToolCalls(message);
|
|
140
|
+
const text = assistantText(message);
|
|
141
|
+
if (!text && toolCalls.length === 0) return undefined;
|
|
142
|
+
return {
|
|
143
|
+
role: "assistant",
|
|
144
|
+
content: text || "",
|
|
145
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (message.role === "toolResult") {
|
|
149
|
+
return {
|
|
150
|
+
role: "tool",
|
|
151
|
+
content: toolResultText(message),
|
|
152
|
+
tool_call_id: message.toolCallId,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function mapOcxToolsToDevin(tools: OcxTool[] | undefined): ToolDef[] | undefined {
|
|
159
|
+
if (!tools || tools.length === 0) return undefined;
|
|
160
|
+
return tools.map((tool) => ({
|
|
161
|
+
name: tool.name,
|
|
162
|
+
description: tool.description ?? "",
|
|
163
|
+
parameters: tool.parameters ?? { type: "object", properties: {} },
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createDevinAdapter(
|
|
168
|
+
provider: OcxProviderConfig,
|
|
169
|
+
context: { providerId?: string } = {},
|
|
170
|
+
): ProviderAdapter {
|
|
171
|
+
// Which credential slot holds this row's tenant. Defaults to `devin` so every
|
|
172
|
+
// existing caller — including the tests that construct this adapter directly —
|
|
173
|
+
// behaves exactly as before.
|
|
174
|
+
const credentialProviderId = context.providerId ?? "devin";
|
|
175
|
+
const cascadeIds = new Map<string, string>();
|
|
176
|
+
const CASCADE_ID_MAX = 256;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
name: "devin",
|
|
180
|
+
|
|
181
|
+
buildRequest() {
|
|
182
|
+
return {
|
|
183
|
+
url: provider.baseUrl || DEVIN_API_SERVER,
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {},
|
|
186
|
+
body: "",
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async *parseStream(): AsyncGenerator<AdapterEvent> {
|
|
191
|
+
yield {
|
|
192
|
+
type: "error",
|
|
193
|
+
message: "Devin adapter uses runTurn; the fetch/parseStream path is disabled.",
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
async runTurn(parsed: OcxParsedRequest, incoming: IncomingMeta, emit: (event: AdapterEvent) => void) {
|
|
198
|
+
if (incoming.abortSignal?.aborted) {
|
|
199
|
+
emit({ type: "error", message: "Devin turn was aborted before start." });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let apiKey: string;
|
|
203
|
+
try {
|
|
204
|
+
apiKey = resolveDevinToken(provider, incoming.headers);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
emit({ type: "error", message: error instanceof Error ? error.message : String(error) });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const threadKey = parsed._clientThreadId || parsed.previousResponseId || "default";
|
|
211
|
+
let cascadeId = cascadeIds.get(threadKey);
|
|
212
|
+
if (!cascadeId) {
|
|
213
|
+
// Evict oldest entries to bound memory in long-running proxy processes.
|
|
214
|
+
if (cascadeIds.size >= CASCADE_ID_MAX) {
|
|
215
|
+
const firstKey = cascadeIds.keys().next().value;
|
|
216
|
+
if (firstKey) cascadeIds.delete(firstKey);
|
|
217
|
+
}
|
|
218
|
+
cascadeId = allocateCascadeId();
|
|
219
|
+
cascadeIds.set(threadKey, cascadeId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const rawModelId = parsed.modelId.includes("/") ? parsed.modelId.slice(parsed.modelId.lastIndexOf("/") + 1) : parsed.modelId;
|
|
223
|
+
// The signed-in account's tenant decides the host, not the static registry
|
|
224
|
+
// entry: an EU or FedStart account that used provider.baseUrl would send
|
|
225
|
+
// every RPC to the US server it is not provisioned on.
|
|
226
|
+
const host = resolveDevinApiServer(provider.baseUrl, credentialProviderId);
|
|
227
|
+
const modelUid = await resolveWireModelUid(rawModelId, apiKey, host, parsed.options.reasoning);
|
|
228
|
+
let openToolId: string | undefined;
|
|
229
|
+
let usage: OcxUsage | undefined;
|
|
230
|
+
let stopReason: string | undefined;
|
|
231
|
+
|
|
232
|
+
const closeOpenTool = () => {
|
|
233
|
+
if (!openToolId) return;
|
|
234
|
+
emit({ type: "tool_call_end" });
|
|
235
|
+
openToolId = undefined;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
for await (const event of streamChatEvents({
|
|
240
|
+
apiKey,
|
|
241
|
+
apiServerUrl: host,
|
|
242
|
+
modelUid,
|
|
243
|
+
messages: mapOcxMessagesToDevin(parsed),
|
|
244
|
+
tools: mapOcxToolsToDevin(parsed.context.tools),
|
|
245
|
+
cascadeId,
|
|
246
|
+
// Without these the request falls back to the encoder's defaults
|
|
247
|
+
// (8192 output, a 128k context window, temperature 0.7), so a client
|
|
248
|
+
// that asked for a 4k cap never got one.
|
|
249
|
+
completionOpts: {
|
|
250
|
+
...(typeof parsed.options.maxOutputTokens === "number" ? { maxOutputTokens: parsed.options.maxOutputTokens } : {}),
|
|
251
|
+
...(typeof parsed.options.temperature === "number" ? { temperature: parsed.options.temperature } : {}),
|
|
252
|
+
...(typeof parsed.options.topP === "number" ? { topP: parsed.options.topP } : {}),
|
|
253
|
+
},
|
|
254
|
+
signal: incoming.abortSignal,
|
|
255
|
+
})) {
|
|
256
|
+
if (incoming.abortSignal?.aborted) {
|
|
257
|
+
// Emitting nothing here left the bridge to synthesize adapter_eof.
|
|
258
|
+
// Say what happened instead, the way the other runTurn-only adapter
|
|
259
|
+
// does, and carry any usage already seen.
|
|
260
|
+
closeOpenTool();
|
|
261
|
+
emit({ type: "error", message: "Devin turn was aborted.", ...(usage ? { usage } : {}) });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (event.kind === "text") {
|
|
265
|
+
closeOpenTool();
|
|
266
|
+
if (event.text) emit({ type: "text_delta", text: event.text });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (event.kind === "reasoning") {
|
|
270
|
+
if (event.text) emit({ type: "thinking_delta", thinking: event.text });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (event.kind === "tool_call_start") {
|
|
274
|
+
closeOpenTool();
|
|
275
|
+
openToolId = event.id;
|
|
276
|
+
emit({ type: "tool_call_start", id: event.id, name: event.name });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (event.kind === "tool_call_args") {
|
|
280
|
+
if (event.argsDelta) emit({ type: "tool_call_delta", arguments: event.argsDelta });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (event.kind === "finish") {
|
|
284
|
+
closeOpenTool();
|
|
285
|
+
// A natural completion carries no stopReason: the bridge reads any
|
|
286
|
+
// truthy value as "this turn did not reach a final answer", so
|
|
287
|
+
// reporting "stop" costs every clean Devin turn its final_answer
|
|
288
|
+
// phase.
|
|
289
|
+
stopReason = event.reason === "length" ? "max_tokens" : event.reason === "stop" ? undefined : event.reason;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (event.kind === "usage") {
|
|
293
|
+
const total = event.totalTokens ?? ((event.promptTokens ?? 0) + (event.completionTokens ?? 0));
|
|
294
|
+
usage = {
|
|
295
|
+
inputTokens: event.promptTokens ?? 0,
|
|
296
|
+
outputTokens: event.completionTokens ?? 0,
|
|
297
|
+
...(total > 0 ? { totalTokens: total } : {}),
|
|
298
|
+
...(event.cachedInputTokens !== undefined ? { cachedInputTokens: event.cachedInputTokens } : {}),
|
|
299
|
+
...(event.cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens: event.cacheCreationInputTokens } : {}),
|
|
300
|
+
...(event.reasoningTokens !== undefined ? { reasoningOutputTokens: event.reasoningTokens } : {}),
|
|
301
|
+
};
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
closeOpenTool();
|
|
306
|
+
if (incoming.abortSignal?.aborted) {
|
|
307
|
+
emit({ type: "error", message: "Devin turn was aborted.", ...(usage ? { usage } : {}) });
|
|
308
|
+
} else {
|
|
309
|
+
emit({ type: "done", ...(usage ? { usage } : {}), ...(stopReason ? { stopReason } : {}) });
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
closeOpenTool();
|
|
313
|
+
if (incoming.abortSignal?.aborted) {
|
|
314
|
+
emit({ type: "error", message: "Devin turn was aborted.", ...(usage ? { usage } : {}) });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const message = error instanceof CloudChatError
|
|
318
|
+
? ("Devin cloud error" + (error.code ? " " + error.code : "") + ": " + error.message)
|
|
319
|
+
: error instanceof Error ? error.message : String(error);
|
|
320
|
+
// Usage that already arrived is still real; dropping it loses the
|
|
321
|
+
// accounting for a turn that did most of its work before failing.
|
|
322
|
+
emit({ type: "error", message, ...(usage ? { usage } : {}) });
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -109,7 +109,7 @@ function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string
|
|
|
109
109
|
* Unknown ids return `undefined` deliberately. An earlier revision returned a 16,384 floor for
|
|
110
110
|
* anything unmatched, which silently truncated aliases, gateway ids, and any model added after
|
|
111
111
|
* this table was written — the operator asked for N tokens and got 16,384 with no signal. A cap
|
|
112
|
-
* we cannot justify is worse than no cap: `structure/
|
|
112
|
+
* we cannot justify is worse than no cap: `structure/config.md` is explicit
|
|
113
113
|
* that an explicit request value wins, so an unrecognized model passes through untouched and the
|
|
114
114
|
* upstream remains the authority on its own limit.
|
|
115
115
|
*
|
|
@@ -96,7 +96,14 @@ function openAIChatTransport(provider: OcxProviderConfig): {
|
|
|
96
96
|
};
|
|
97
97
|
if (hasCredential) headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
98
98
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
99
|
-
|
|
99
|
+
// A configured relative path wins, mirroring how the Responses adapter honours
|
|
100
|
+
// `responsesPath`. An upstream can serve both wires under different prefixes, and a
|
|
101
|
+
// per-model wire override only swaps the adapter, so without this the opted-in Chat
|
|
102
|
+
// request would be sent to the Responses base with `/chat/completions` appended.
|
|
103
|
+
const url = provider.chatCompletionsPath === undefined
|
|
104
|
+
? openaiChatCompletionsUrl(provider.baseUrl)
|
|
105
|
+
: `${provider.baseUrl.replace(/\/$/, "")}${provider.chatCompletionsPath}`;
|
|
106
|
+
return { url, headers, hasCredential };
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
/**
|
|
@@ -140,6 +147,16 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
140
147
|
// ingress enforces exactly that. A prefix match here would strip response_format from
|
|
141
148
|
// `<listed>:<tag>` siblings the operator never opted out, silently returning prose.
|
|
142
149
|
if (provider.noStructuredOutputModels?.includes(modelId)) delete body.response_format;
|
|
150
|
+
// Narrower neighbour: the model takes `json_object` but rejects `json_schema`. Downgrade
|
|
151
|
+
// rather than drop, so a caller that asked for JSON still gets JSON. The type check also
|
|
152
|
+
// makes the kill switch above win without an else — after its `delete` there is no type
|
|
153
|
+
// left to match.
|
|
154
|
+
const passthroughFormat = body.response_format;
|
|
155
|
+
if (provider.noJsonSchemaModels?.includes(modelId)
|
|
156
|
+
&& typeof passthroughFormat === "object" && passthroughFormat !== null
|
|
157
|
+
&& (passthroughFormat as { type?: unknown }).type === "json_schema") {
|
|
158
|
+
body.response_format = { type: "json_object" };
|
|
159
|
+
}
|
|
143
160
|
|
|
144
161
|
// Run the same complete Fast policy as the translated Chat path, including explicit
|
|
145
162
|
// fastMode and foreign-tier handling. On inherited canonical Fast, the passthrough still
|
|
@@ -1582,15 +1599,19 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1582
1599
|
if (textFormat?.type === "json_object") {
|
|
1583
1600
|
body.response_format = { type: "json_object" };
|
|
1584
1601
|
} else if (textFormat?.type === "json_schema") {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1602
|
+
// Same downgrade as the passthrough path: the schema is dropped because the
|
|
1603
|
+
// upstream rejects it, but the JSON-mode request itself survives.
|
|
1604
|
+
body.response_format = provider.noJsonSchemaModels?.includes(parsed.modelId)
|
|
1605
|
+
? { type: "json_object" }
|
|
1606
|
+
: {
|
|
1607
|
+
type: "json_schema",
|
|
1608
|
+
json_schema: {
|
|
1609
|
+
name: textFormat.name ?? "response",
|
|
1610
|
+
...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
|
|
1611
|
+
...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
|
|
1612
|
+
...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
|
|
1613
|
+
},
|
|
1614
|
+
};
|
|
1594
1615
|
}
|
|
1595
1616
|
}
|
|
1596
1617
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeRoutedAgentMessages } from "./routed-agent-messages";
|
|
2
|
+
import { stripBracketedModelSuffix } from "./openai-chat";
|
|
2
3
|
import { normalizeOpenCodeGoAdditionalTools } from "./opencode-go-additional-tools";
|
|
3
4
|
import { isXaiResponsesDestination } from "../providers/xai-transport";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
@@ -2541,7 +2542,21 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2541
2542
|
actualServiceTier === null ? null : "service-tier",
|
|
2542
2543
|
actualServiceTier,
|
|
2543
2544
|
);
|
|
2544
|
-
|
|
2545
|
+
// The Responses adapter is passthrough: it forwards `parsed._rawBody` rather than
|
|
2546
|
+
// rebuilding the body from `parsed.modelId`, and the router writes the routed id into
|
|
2547
|
+
// that raw body. So a provider whose upstream rejects bracketed ids has to be honoured
|
|
2548
|
+
// here, on the serialized body, not on the parsed selector. One place covers both the
|
|
2549
|
+
// HTTP and the WebSocket outbound, because the WS path transports this same request
|
|
2550
|
+
// instead of rebuilding it.
|
|
2551
|
+
const body = JSON.stringify(
|
|
2552
|
+
provider.modelSuffixBracketStrip
|
|
2553
|
+
&& finalBody !== null
|
|
2554
|
+
&& typeof finalBody === "object"
|
|
2555
|
+
&& !Array.isArray(finalBody)
|
|
2556
|
+
&& typeof (finalBody as { model?: unknown }).model === "string"
|
|
2557
|
+
? { ...(finalBody as Record<string, unknown>), model: stripBracketedModelSuffix((finalBody as { model: string }).model) }
|
|
2558
|
+
: finalBody,
|
|
2559
|
+
);
|
|
2545
2560
|
const releaseBodyObservation = translatorBudget.observeExternallyCapped(
|
|
2546
2561
|
"passthrough_serialization",
|
|
2547
2562
|
new TextEncoder().encode(body).byteLength,
|
package/src/adapters/registry.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { createCodeBuddyAdapter } from "./codebuddy/adapter";
|
|
|
6
6
|
import { createQoderAdapter } from "./qoder/adapter";
|
|
7
7
|
import { createCommandCodeAdapter } from "./command-code";
|
|
8
8
|
import { createCursorAdapter } from "./cursor";
|
|
9
|
+
import { createDevinCliAdapter } from "./devin-cli/adapter";
|
|
10
|
+
import { createDevinAdapter } from "./devin";
|
|
9
11
|
import { createGoogleAdapter } from "./google";
|
|
10
12
|
import { createKiroAdapter } from "./kiro";
|
|
11
13
|
import { createMimoFreeAdapter } from "./mimo-free";
|
|
@@ -19,6 +21,16 @@ export type AdapterCacheRetention = "none" | "short" | "long";
|
|
|
19
21
|
|
|
20
22
|
export interface AdapterFactoryContext {
|
|
21
23
|
cacheRetention?: AdapterCacheRetention;
|
|
24
|
+
/**
|
|
25
|
+
* The configured provider row this adapter serves.
|
|
26
|
+
*
|
|
27
|
+
* Needed when one adapter backs two provider ids whose credentials differ:
|
|
28
|
+
* `devin` and `devin-cli` share a transport and a token format but sign in to
|
|
29
|
+
* different accounts and can sit on different Cognition tenants, and the tenant
|
|
30
|
+
* is recorded on the credential rather than in the registry. Optional, and
|
|
31
|
+
* every other adapter ignores it.
|
|
32
|
+
*/
|
|
33
|
+
providerId?: string;
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
export type AdapterWire =
|
|
@@ -30,7 +42,9 @@ export type AdapterWire =
|
|
|
30
42
|
| "openai-responses"
|
|
31
43
|
| "google"
|
|
32
44
|
| "kiro"
|
|
33
|
-
| "cursor"
|
|
45
|
+
| "cursor"
|
|
46
|
+
| "devin-cli"
|
|
47
|
+
| "devin";
|
|
34
48
|
|
|
35
49
|
export type AdapterMutationContract =
|
|
36
50
|
| "codex-owned"
|
|
@@ -112,6 +126,16 @@ export const ADAPTER_REGISTRY = {
|
|
|
112
126
|
mutation: "codex-owned-with-gated-native-fallback",
|
|
113
127
|
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createCursorAdapter(provider),
|
|
114
128
|
},
|
|
129
|
+
"devin-cli": {
|
|
130
|
+
wire: "devin-cli",
|
|
131
|
+
mutation: "codex-owned",
|
|
132
|
+
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createDevinCliAdapter(provider),
|
|
133
|
+
},
|
|
134
|
+
devin: {
|
|
135
|
+
wire: "devin",
|
|
136
|
+
mutation: "codex-owned",
|
|
137
|
+
create: (provider: OcxProviderConfig, context: AdapterFactoryContext) => createDevinAdapter(provider, context),
|
|
138
|
+
},
|
|
115
139
|
"mimo-free": {
|
|
116
140
|
contractParent: "openai-chat",
|
|
117
141
|
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createMimoFreeAdapter(provider),
|
package/src/bridge.ts
CHANGED
|
@@ -1293,7 +1293,10 @@ export function bridgeToResponsesSSE(
|
|
|
1293
1293
|
if (currentReasoning) closeCurrentReasoning();
|
|
1294
1294
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1295
1295
|
flushHiddenRawReasoning();
|
|
1296
|
-
if (currentToolCall)
|
|
1296
|
+
if (currentToolCall) {
|
|
1297
|
+
if (isTruncatedStopReason(event.stopReason)) failCurrentToolCall();
|
|
1298
|
+
else closeCurrentToolCall();
|
|
1299
|
+
}
|
|
1297
1300
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
1298
1301
|
releasePendingWebSources();
|
|
1299
1302
|
// Redacted-only turns (or hidden thinking without a trailing signature event) still
|
|
@@ -2055,7 +2058,10 @@ function buildResponseJSONWithBudget(
|
|
|
2055
2058
|
// must one left open by a stream that stopped without any terminal at all. That case previously
|
|
2056
2059
|
// fell through to "completed", handing back a function_call whose arguments were half-written
|
|
2057
2060
|
// JSON, inside a turn also marked completed.
|
|
2058
|
-
if (currentToolCallId)
|
|
2061
|
+
if (currentToolCallId) {
|
|
2062
|
+
flushToolCall(errorEvent || incompleteEvent || !sawTerminal || isTruncatedStopReason(rawStopReason)
|
|
2063
|
+
? "incomplete" : "completed");
|
|
2064
|
+
}
|
|
2059
2065
|
if (batchKiroRedacted) {
|
|
2060
2066
|
// pushOutput reserves the item itself and releases the retained raw blob it replaces.
|
|
2061
2067
|
pushOutput({
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code appends growing `<total_tokens>…</total_tokens>` footers (and
|
|
3
|
+
* occasional TaskCreate nudges) into system text that becomes Responses
|
|
4
|
+
* `instructions`. That churn breaks Muse/Go prefix cache on the instructions
|
|
5
|
+
* prefix even when tools stay stable. Strip dynamics from instructions;
|
|
6
|
+
* surface the latest notice on `input` instead.
|
|
7
|
+
*
|
|
8
|
+
* Relocation is identified by harness shape: only a trailing, unfenced,
|
|
9
|
+
* canonical notice at the end of instructions is moved. An unmatched fence
|
|
10
|
+
* opener covers through EOF. No match → the original string is returned
|
|
11
|
+
* byte-for-byte. The matcher is content identity only; `translateAnthropicRequest`
|
|
12
|
+
* requires `claudeCode.stabilizePromptCache: true` before this helper runs. Claude Code
|
|
13
|
+
* writes `<total_tokens>N tokens left</total_tokens>`.
|
|
14
|
+
*
|
|
15
|
+
* TaskCreate nudge text has drifted across Claude Code builds; match the
|
|
16
|
+
* known exact paragraphs (legacy + 2.1.263 "tracking progress" form) as
|
|
17
|
+
* trailing alternatives so a mid-session nudge cannot pin older footers
|
|
18
|
+
* above it and flip `instructions` after a previously stable peel.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const TOTAL_NOTICE_RE = /^<total_tokens>[0-9]+ tokens left<\/total_tokens>$/;
|
|
22
|
+
|
|
23
|
+
/** Legacy Claude Code TaskCreate reminder (pre-"tracking progress"). */
|
|
24
|
+
const TASKCREATE_NUDGE_LEGACY =
|
|
25
|
+
"The task tools haven't been used recently. If you're working on tasks that would benefit from tracking, consider using TaskCreate to add them. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable.";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Claude Code 2.1.263+ TaskCreate reminder observed on hitrate S/T4
|
|
29
|
+
* (FREEZE-DIFF + tip0847 pilots). Mentions TaskUpdate and stale-list cleanup.
|
|
30
|
+
*/
|
|
31
|
+
const TASKCREATE_NUDGE_CC_2_1_263 =
|
|
32
|
+
"The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate to add new tasks and TaskUpdate to update task status (set to in_progress when starting, completed when done). Also consider cleaning up the task list if it has become stale. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable.";
|
|
33
|
+
|
|
34
|
+
interface FenceRange {
|
|
35
|
+
start: number;
|
|
36
|
+
end: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FENCE_OPEN_RE = /^( {0,3})(`{3,}|~{3,})/;
|
|
40
|
+
const FENCE_CLOSE_RE = /^( {0,3})(`{3,}|~{3,})[ \t]*$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Markdown fence ranges. An unmatched opener covers through EOF: unfinished
|
|
44
|
+
* fenced examples are code content, not a harness suffix. A closer must be a
|
|
45
|
+
* standalone fence line (no info string) of the same character and at least
|
|
46
|
+
* the opener's length.
|
|
47
|
+
*/
|
|
48
|
+
function fencedRanges(source: string): FenceRange[] {
|
|
49
|
+
const ranges: FenceRange[] = [];
|
|
50
|
+
let offset = 0;
|
|
51
|
+
let openAt: number | null = null;
|
|
52
|
+
let openFence = "";
|
|
53
|
+
while (offset <= source.length) {
|
|
54
|
+
const nl = source.indexOf("\n", offset);
|
|
55
|
+
const lineEnd = nl === -1 ? source.length : nl;
|
|
56
|
+
const line = source.slice(offset, lineEnd).replace(/\r$/, "");
|
|
57
|
+
if (openAt === null) {
|
|
58
|
+
const open = FENCE_OPEN_RE.exec(line);
|
|
59
|
+
if (open) {
|
|
60
|
+
openAt = offset;
|
|
61
|
+
openFence = open[2]!;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const close = FENCE_CLOSE_RE.exec(line);
|
|
65
|
+
if (
|
|
66
|
+
close
|
|
67
|
+
&& close[2]![0] === openFence[0]
|
|
68
|
+
&& close[2]!.length >= openFence.length
|
|
69
|
+
) {
|
|
70
|
+
ranges.push({ start: openAt, end: lineEnd });
|
|
71
|
+
openAt = null;
|
|
72
|
+
openFence = "";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (nl === -1) break;
|
|
76
|
+
offset = nl + 1;
|
|
77
|
+
}
|
|
78
|
+
if (openAt !== null) ranges.push({ start: openAt, end: source.length });
|
|
79
|
+
return ranges;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A backwards cursor consumes each line and fence range at most once. */
|
|
83
|
+
export function stabilizeClaudeInstructionsForPromptCache(
|
|
84
|
+
instructions: string,
|
|
85
|
+
): { instructions: string; dynamicNotice: string | null } {
|
|
86
|
+
const ranges = fencedRanges(instructions);
|
|
87
|
+
let fenceIndex = ranges.length - 1;
|
|
88
|
+
let end = instructions.length;
|
|
89
|
+
let latestTotal: string | null = null;
|
|
90
|
+
let latestNudge: string | null = null;
|
|
91
|
+
let peeled = false;
|
|
92
|
+
|
|
93
|
+
for (;;) {
|
|
94
|
+
// This is speculative: a failed candidate must not trim the retained prefix.
|
|
95
|
+
let lineEnd = end;
|
|
96
|
+
while (lineEnd > 0 && instructions[lineEnd - 1] === "\n") {
|
|
97
|
+
lineEnd--;
|
|
98
|
+
if (lineEnd > 0 && instructions[lineEnd - 1] === "\r") lineEnd--;
|
|
99
|
+
}
|
|
100
|
+
if (lineEnd === 0) break;
|
|
101
|
+
const lineStart = instructions.lastIndexOf("\n", lineEnd - 1) + 1;
|
|
102
|
+
let contentStart = lineStart;
|
|
103
|
+
let contentEnd = lineEnd;
|
|
104
|
+
while (contentStart < contentEnd && (instructions[contentStart] === " " || instructions[contentStart] === "\t")) contentStart++;
|
|
105
|
+
while (contentEnd > contentStart && (instructions[contentEnd - 1] === " " || instructions[contentEnd - 1] === "\t")) contentEnd--;
|
|
106
|
+
const notice = instructions.slice(contentStart, contentEnd);
|
|
107
|
+
const total = TOTAL_NOTICE_RE.test(notice);
|
|
108
|
+
const nudge = notice === TASKCREATE_NUDGE_LEGACY || notice === TASKCREATE_NUDGE_CC_2_1_263;
|
|
109
|
+
if (!total && !nudge) break;
|
|
110
|
+
|
|
111
|
+
while (fenceIndex >= 0 && ranges[fenceIndex]!.start > contentStart) fenceIndex--;
|
|
112
|
+
if (fenceIndex >= 0 && contentStart < ranges[fenceIndex]!.end) break;
|
|
113
|
+
if (total && latestTotal === null) latestTotal = notice;
|
|
114
|
+
if (nudge && latestNudge === null) latestNudge = notice;
|
|
115
|
+
peeled = true;
|
|
116
|
+
|
|
117
|
+
// Commit only the recognized notice and its immediately preceding LF/CRLF separators.
|
|
118
|
+
end = lineStart;
|
|
119
|
+
while (end > 0 && instructions[end - 1] === "\n") {
|
|
120
|
+
end--;
|
|
121
|
+
if (end > 0 && instructions[end - 1] === "\r") end--;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!peeled) return { instructions, dynamicNotice: null };
|
|
126
|
+
const noticeParts: string[] = [];
|
|
127
|
+
if (latestTotal) noticeParts.push(latestTotal);
|
|
128
|
+
if (latestNudge) noticeParts.push(latestNudge);
|
|
129
|
+
return { instructions: instructions.slice(0, end), dynamicNotice: noticeParts.join("\n\n") };
|
|
130
|
+
}
|