@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923

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.
Files changed (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-EJxiUFMq.js +0 -50
  259. package/gui/dist/assets/index-BmJwNBHL.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BmJwNBHL.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-DdDunwDb.css">
19
+ <script type="module" crossorigin src="/assets/index-BEq4OCOz.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-DiBRuK-d.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -91,7 +91,10 @@ impl HelperRequest {
91
91
  Ok(())
92
92
  }
93
93
 
94
+ // Staged for the Windows confinement backend, which still reports unavailable; nothing calls
95
+ // it yet, so the lint gate would otherwise fail every Windows build.
94
96
  #[cfg(target_os = "windows")]
97
+ #[allow(dead_code)]
95
98
  pub fn canonical_paths(&self) -> Result<CanonicalPaths, String> {
96
99
  let root = canonical_directory(&self.root, "workspace root")?;
97
100
  let cwd = canonical_directory(&self.cwd, "command cwd")?;
@@ -121,6 +124,7 @@ fn validate_path(value: &str, label: &str) -> Result<(), String> {
121
124
  }
122
125
 
123
126
  #[cfg(target_os = "windows")]
127
+ #[allow(dead_code)]
124
128
  fn canonical_directory(value: &str, label: &str) -> Result<PathBuf, String> {
125
129
  let original = Path::new(value);
126
130
  let metadata =
@@ -134,6 +138,7 @@ fn canonical_directory(value: &str, label: &str) -> Result<PathBuf, String> {
134
138
  }
135
139
 
136
140
  #[cfg(target_os = "windows")]
141
+ #[allow(dead_code)]
137
142
  #[derive(Debug)]
138
143
  pub struct CanonicalPaths {
139
144
  pub root: PathBuf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.63.0-preview.20260923",
3
+ "version": "2.64.0-preview.20260923",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -20,6 +20,9 @@
20
20
  "src",
21
21
  "gui/dist",
22
22
  "assets/banner.png",
23
+ "assets/download-macos.svg",
24
+ "assets/download-windows.svg",
25
+ "assets/download-linux.svg",
23
26
  "assets/architecture.png",
24
27
  "assets/claude-code-models.gif",
25
28
  "assets/codex-app-picker.png",
@@ -1,4 +1,6 @@
1
1
  import type { IncomingMeta, ProviderAdapter } from "./base";
2
+ import { createAdapterTierMetadata, type AdapterTierMetadata } from "../providers/fastwire";
3
+ import { ANTHROPIC_FAST_MODE_BETA, mergeAnthropicBetaHeader } from "../providers/anthropic-fast";
2
4
  import { createToolCallIdAllocator, type ToolCallIdAllocator } from "./tool-call-id";
3
5
  import { debugDroppedFrame } from "../lib/debug";
4
6
  import type {
@@ -175,6 +177,31 @@ function applyPromptCaching(
175
177
  }
176
178
  }
177
179
 
180
+ /**
181
+ * The fast-mode wire this request should carry, if any. Only a settled `set` decision on a
182
+ * declared `anthropic-speed` wire emits it, and only with the value that wire maps canonical
183
+ * Fast to; everything else (drop, forwarded caller tiers, a declaration for another wire)
184
+ * sends no `speed`, which is the API's standard speed.
185
+ */
186
+ function anthropicFastSpeed(
187
+ parsed: OcxParsedRequest,
188
+ provider: OcxProviderConfig,
189
+ ): { value: string; betas: readonly string[] } | undefined {
190
+ const decision = parsed.options.tierDecision;
191
+ if (decision?.kind !== "set") return undefined;
192
+ const wire = parsed.options.tierObservation?.fastWire ?? provider.fastWire ?? null;
193
+ if (wire?.kind !== "anthropic-speed") return undefined;
194
+ if (decision.value !== wire.canonicalToWire.priority) return undefined;
195
+ return { value: decision.value, betas: wire.betas?.length ? wire.betas : [ANTHROPIC_FAST_MODE_BETA] };
196
+ }
197
+
198
+ /** Feed a `usage.speed` echo ("fast" | "standard") to the attempt's tier observer. */
199
+ function observeAnthropicSpeed(usage: unknown, tierMetadata: AdapterTierMetadata | undefined): void {
200
+ if (!tierMetadata || !usage || typeof usage !== "object" || Array.isArray(usage)) return;
201
+ const speed = (usage as { speed?: unknown }).speed;
202
+ if (speed !== undefined) tierMetadata.observeResponseServiceTier(speed);
203
+ }
204
+
178
205
  // ---------------------------------------------------------------------------
179
206
  // Breakpoint cap enforcement — strip excess beyond the 4-breakpoint limit
180
207
  // ---------------------------------------------------------------------------
@@ -1088,6 +1115,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1088
1115
  if (unresolvedPlaceholder) {
1089
1116
  throw new Error(`anthropic baseUrl contains unresolved ${unresolvedPlaceholder}`);
1090
1117
  }
1118
+ // Anthropic fast mode: `speed` is only accepted beside its beta; without it the API
1119
+ // answers 400 "speed: Extra inputs are not permitted". The beta is merged below, after
1120
+ // any header override, so a request never carries one without the other.
1121
+ const fastSpeed = anthropicFastSpeed(parsed, provider);
1122
+ if (fastSpeed) body.speed = fastSpeed.value;
1091
1123
  const headers: Record<string, string> = {
1092
1124
  "Content-Type": "application/json",
1093
1125
  "anthropic-version": "2023-06-01",
@@ -1108,6 +1140,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1108
1140
  else headers["x-api-key"] = provider.apiKey;
1109
1141
  }
1110
1142
  if (provider.headers) Object.assign(headers, provider.headers);
1143
+ mergeAnthropicBetaHeader(headers, fastSpeed?.betas ?? []);
1111
1144
 
1112
1145
  // Prompt caching: native Anthropic supports top-level automatic caching, which
1113
1146
  // follows the moving final block across turns. Keep one breakpoint slot free for it.
@@ -1122,10 +1155,27 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1122
1155
  enforceCacheControlLimit(body, explicitLimit);
1123
1156
  normalizeTtlOrdering(body);
1124
1157
 
1125
- return { url, method: "POST", headers, body: JSON.stringify(body) };
1158
+ return {
1159
+ url,
1160
+ method: "POST",
1161
+ headers,
1162
+ body: JSON.stringify(body),
1163
+ // Adapter-owned: the registry wrapper would otherwise report the exact absence of a
1164
+ // tier field and mislabel every fast turn as downgraded.
1165
+ tierLog: createAdapterTierMetadata(
1166
+ parsed.options.tierObservation,
1167
+ parsed.options.tierDecision,
1168
+ fastSpeed ? "anthropic-speed" : null,
1169
+ fastSpeed?.value ?? null,
1170
+ ),
1171
+ };
1126
1172
  },
1127
1173
 
1128
- async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
1174
+ async *parseStream(
1175
+ response: Response,
1176
+ budget: TranslatorBudget,
1177
+ tierMetadata?: AdapterTierMetadata,
1178
+ ): AsyncGenerator<AdapterEvent> {
1129
1179
  if (!response.body) {
1130
1180
  yield { type: "error", message: "No response body" };
1131
1181
  return;
@@ -1199,10 +1249,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1199
1249
  case "message_start": {
1200
1250
  const message = data.message as { usage?: unknown } | undefined;
1201
1251
  pendingUsage = mergeAnthropicUsage(pendingUsage, message?.usage);
1252
+ // Fast mode reports the speed actually served here (`usage.speed`).
1253
+ observeAnthropicSpeed(message?.usage, tierMetadata);
1202
1254
  break;
1203
1255
  }
1204
1256
  case "content_block_start": {
1205
- const block = data.content_block as { type: string; id?: string; name?: string; data?: string; thinking?: string } | undefined;
1257
+ const block = data.content_block as { type: string; id?: string; name?: unknown; data?: string; thinking?: string } | undefined;
1206
1258
  if (!block) break;
1207
1259
  currentBlockType = block.type;
1208
1260
  if (block.type === "thinking") {
@@ -1212,7 +1264,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1212
1264
  }
1213
1265
  if (block.type === "tool_use") {
1214
1266
  currentToolCallId = usableToolUseId(block.id);
1215
- currentToolCallName = toolNames.fromWire(block.name ?? "");
1267
+ currentToolCallName = toolNames.fromWire(typeof block.name === "string" ? block.name : "");
1216
1268
  currentToolCallJson = "";
1217
1269
  budget.openCall(currentToolCallId);
1218
1270
  yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName };
@@ -1288,6 +1340,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1288
1340
  case "message_delta": {
1289
1341
  const usage = data.usage;
1290
1342
  pendingUsage = mergeAnthropicUsage(pendingUsage, usage);
1343
+ // Later frames win: a speed here (not seen live, but usage is cumulative) supersedes message_start.
1344
+ observeAnthropicSpeed(usage, tierMetadata);
1291
1345
  const delta = data.delta as { stop_reason?: unknown } | undefined;
1292
1346
  if (typeof delta?.stop_reason === "string") pendingStopReason = delta.stop_reason;
1293
1347
  break;
@@ -1377,7 +1431,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1377
1431
  }
1378
1432
  },
1379
1433
 
1380
- async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1434
+ async parseResponse(
1435
+ response: Response,
1436
+ budget: TranslatorBudget,
1437
+ tierMetadata?: AdapterTierMetadata,
1438
+ ): Promise<AdapterEvent[]> {
1381
1439
  const parsed: unknown = await response.json();
1382
1440
  // `response.json()` resolves a body of `null` to `null` without throwing, so the cast below
1383
1441
  // used to reach `json.content` on it — the #1219 defect at the buffered body root. The
@@ -1426,7 +1484,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1426
1484
  }
1427
1485
  }
1428
1486
  }
1429
- const content = rawContent as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
1487
+ const content = rawContent as { type: string; text?: string; id?: string; name?: unknown; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
1430
1488
  if (content) {
1431
1489
  for (const block of content) {
1432
1490
  if (block.type === "text" && block.text) {
@@ -1442,13 +1500,14 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1442
1500
  events.push({ type: "redacted_thinking", data: block.data });
1443
1501
  } else if (block.type === "tool_use") {
1444
1502
  const id = usableToolUseId(block.id);
1445
- events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") });
1503
+ events.push({ type: "tool_call_start", id, name: toolNames.fromWire(typeof block.name === "string" ? block.name : "") });
1446
1504
  events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input, provider.anthropicEofTolerance === true) });
1447
1505
  events.push({ type: "tool_call_end" });
1448
1506
  }
1449
1507
  }
1450
1508
  }
1451
1509
  const usage = json.usage as Record<string, number> | undefined;
1510
+ observeAnthropicSpeed(json.usage, tierMetadata);
1452
1511
  const stopReason = typeof json.stop_reason === "string" ? json.stop_reason : undefined;
1453
1512
  // An Anthropic-compatible upstream can forward an `error` stop reason verbatim. As a
1454
1513
  // `done` it reads as a clean completion, so the turn reports success and — on a compaction
@@ -1,14 +1,45 @@
1
1
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
2
6
  import type { AdapterRequest, ProviderAdapter } from "../base";
3
7
  import { mapReasoningEffort } from "../../reasoning-effort";
4
8
  import { buildSystemPrompt } from "../coding-agent/protocol";
5
- import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type SpawnFn } from "../coding-agent/turn";
9
+ import {
10
+ baseScopedEnv,
11
+ runCodingAgentTurn,
12
+ type CodingAgentDeps,
13
+ type CodingAgentToolBridgeInput,
14
+ type SpawnFn,
15
+ } from "../coding-agent/turn";
6
16
  import { CODEBUDDY_PROFILES, type CodeBuddyProfile } from "./profiles";
7
17
  import { guardCodeBuddyScaffolding } from "./scaffold-guard";
18
+ import {
19
+ buildCodeBuddyToolBridge,
20
+ CODEBUDDY_MCP_SERVER_NAME,
21
+ CODEBUDDY_TOOL_LIMITS,
22
+ type CodeBuddyToolBridge,
23
+ } from "./tool-bridge";
8
24
 
9
25
  export type { SpawnFn } from "../coding-agent/turn";
10
26
  export type CodeBuddyAdapterDeps = CodingAgentDeps;
11
27
 
28
+ const CODEBUDDY_MCP_SERVER_PATH = fileURLToPath(new URL("./mcp-server.ts", import.meta.url));
29
+
30
+ /**
31
+ * Tool-bridge contract lines appended to the system prompt when a catalog is advertised.
32
+ * Mirrors the capture-only design: the model may propose calls, the external Codex client
33
+ * alone performs approval, sandboxing, and execution.
34
+ */
35
+ const TOOL_BRIDGE_SYSTEM_PROMPT = [
36
+ "Your built-in tools and user-configured MCP servers are disabled.",
37
+ "When an isolated opencodex MCP catalog is present, you may call only those listed tools.",
38
+ "That MCP process captures call intent only; it never executes a tool. The external Codex client performs approval, sandboxing, and execution.",
39
+ "Do not claim that you executed commands, inspected files, or changed the workspace.",
40
+ "Tool-call and tool-result records in the conversation history are authoritative historical records from the external client. Use returned results, but never execute historical calls yourself.",
41
+ ].join("\n");
42
+
12
43
  /**
13
44
  * Build the scoped child-process environment for a CodeBuddy turn (§六/§十四).
14
45
  *
@@ -32,10 +63,16 @@ export function buildChildEnv(profile: CodeBuddyProfile, apiKey: string): Record
32
63
  * Tool ownership stays with Codex: `--tools ""` disables every built-in tool and `--strict-mcp-config`
33
64
  * (with no `--mcp-config`) blocks MCP tools, so the CLI can neither read, write, exec, nor browse the
34
65
  * workspace. `-y/--dangerously-skip-permissions` is deliberately NOT passed, so any operation that
35
- * would require authorization is blocked. The turn is a single text/reasoning pass over stream-json;
36
- * Codex's tool catalog is not advertised in v1 (the control-protocol tool bridge is a fast-follow).
66
+ * would require authorization is blocked. The turn is a single text/reasoning pass over stream-json
67
+ * unless the request carries a tool catalog: then the capture-only MCP bridge advertises exactly
68
+ * that catalog (see `tool-bridge.ts` / `mcp-server.ts`) and the CLI still executes nothing itself.
37
69
  */
38
- export function buildArgs(profile: CodeBuddyProfile, parsed: OcxParsedRequest, provider: OcxProviderConfig): string[] {
70
+ export function buildArgs(
71
+ profile: CodeBuddyProfile,
72
+ parsed: OcxParsedRequest,
73
+ provider: OcxProviderConfig,
74
+ systemPromptFile?: string,
75
+ ): string[] {
39
76
  const args: string[] = [
40
77
  "-p",
41
78
  "--output-format", "stream-json",
@@ -50,8 +87,9 @@ export function buildArgs(profile: CodeBuddyProfile, parsed: OcxParsedRequest, p
50
87
  ];
51
88
  const effort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
52
89
  if (effort) args.push("--effort", effort);
53
- const system = buildSystemPrompt(parsed);
54
- if (system) args.push("--append-system-prompt", system);
90
+ // The vendor CLI documents no file-backed append flag, so the staged prompt replaces the default.
91
+ // That default targets interactive tool use, which this adapter disables end to end.
92
+ if (systemPromptFile) args.push("--system-prompt-file", systemPromptFile);
55
93
  // profile is retained for symmetry with the region-isolated design and future per-region flags.
56
94
  void profile;
57
95
  return args;
@@ -71,16 +109,74 @@ export function createCodeBuddyAdapter(provider: OcxProviderConfig, deps: CodeBu
71
109
  },
72
110
 
73
111
  async runTurn(parsed, incoming, emit): Promise<void> {
74
- await runCodingAgentTurn({
75
- profiles: CODEBUDDY_PROFILES,
76
- provider,
77
- parsed,
78
- incoming,
79
- emit: guardCodeBuddyScaffolding(emit),
80
- buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov),
81
- buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey),
82
- deps,
83
- });
112
+ let toolBridge: CodeBuddyToolBridge;
113
+ try {
114
+ toolBridge = buildCodeBuddyToolBridge(parsed);
115
+ } catch (err) {
116
+ emit({
117
+ type: "error",
118
+ message: `Invalid CodeBuddy tool catalog: ${err instanceof Error ? err.message : String(err)}`,
119
+ status: 400,
120
+ errorType: "invalid_request_error",
121
+ code: "tool_catalog_invalid",
122
+ retryable: false,
123
+ });
124
+ return;
125
+ }
126
+ const bridgeInput: CodingAgentToolBridgeInput | undefined = toolBridge.tools.length > 0
127
+ ? {
128
+ serverName: CODEBUDDY_MCP_SERVER_NAME,
129
+ serverModulePath: CODEBUDDY_MCP_SERVER_PATH,
130
+ tools: toolBridge.tools,
131
+ emittedNameMap: toolBridge.emittedNameMap,
132
+ maxTurnToolCalls: CODEBUDDY_TOOL_LIMITS.maxTurnToolCalls,
133
+ requireToolCall: toolBridge.requireToolCall,
134
+ }
135
+ : undefined;
136
+ // argv is world-readable via process listing, so the folded system+developer prompt —
137
+ // plus the tool-bridge directive when a catalog is advertised — is staged in a
138
+ // private temp file and passed by path instead of embedded in the arguments.
139
+ const system = buildSystemPrompt(parsed);
140
+ const systemParts: string[] = [];
141
+ if (system) systemParts.push(system);
142
+ if (toolBridge.tools.length > 0) systemParts.push(TOOL_BRIDGE_SYSTEM_PROMPT);
143
+ const staged = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
144
+ let promptDir: string | undefined;
145
+ let promptFile: string | undefined;
146
+ if (staged) {
147
+ try {
148
+ promptDir = await mkdtemp(join(tmpdir(), "ocx-codebuddy-prompt-"));
149
+ promptFile = join(promptDir, "system-prompt.txt");
150
+ await writeFile(promptFile, staged, { encoding: "utf8", mode: 0o600, flag: "wx" });
151
+ } catch {
152
+ if (promptDir) await rm(promptDir, { recursive: true, force: true }).catch(() => {});
153
+ emit({
154
+ type: "error",
155
+ message: "CodeBuddy system prompt could not be staged securely.",
156
+ status: 500,
157
+ errorType: "upstream_error",
158
+ code: "system_prompt_staging_failed",
159
+ retryable: false,
160
+ });
161
+ return;
162
+ }
163
+ }
164
+
165
+ try {
166
+ await runCodingAgentTurn({
167
+ profiles: CODEBUDDY_PROFILES,
168
+ provider,
169
+ parsed,
170
+ incoming,
171
+ emit: guardCodeBuddyScaffolding(emit),
172
+ ...(bridgeInput ? { toolBridge: bridgeInput } : {}),
173
+ buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov, promptFile),
174
+ buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey),
175
+ deps,
176
+ });
177
+ } finally {
178
+ if (promptDir) await rm(promptDir, { recursive: true, force: true }).catch(() => {});
179
+ }
84
180
  },
85
181
  };
86
182
  }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Isolated MCP catalog used by the CodeBuddy adapter.
3
+ *
4
+ * This process advertises the current Codex tool schemas but deliberately never
5
+ * executes a call. The parent adapter captures CodeBuddy's completed `tool_use`
6
+ * frame, terminates this process tree, and returns the call to the Codex host,
7
+ * where the normal approval and sandbox boundary remains authoritative.
8
+ */
9
+
10
+ import { open } from "node:fs/promises";
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import {
14
+ CallToolRequestSchema,
15
+ ListToolsRequestSchema,
16
+ } from "@modelcontextprotocol/sdk/types.js";
17
+ import { CODEBUDDY_TOOL_LIMITS } from "./tool-bridge";
18
+
19
+ interface ToolDefinition {
20
+ name: string;
21
+ description: string;
22
+ inputSchema: Record<string, unknown>;
23
+ }
24
+
25
+ const MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;
26
+ const INVALID_DESCRIPTION_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
27
+ const textEncoder = new TextEncoder();
28
+
29
+ function isRecord(value: unknown): value is Record<string, unknown> {
30
+ return value !== null && typeof value === "object" && !Array.isArray(value);
31
+ }
32
+
33
+ function utf8Bytes(value: string): number {
34
+ return textEncoder.encode(value).byteLength;
35
+ }
36
+
37
+ function hasUnpairedSurrogate(value: string): boolean {
38
+ for (let index = 0; index < value.length; index++) {
39
+ const unit = value.charCodeAt(index);
40
+ if (unit >= 0xd800 && unit <= 0xdbff) {
41
+ const next = value.charCodeAt(index + 1);
42
+ if (next < 0xdc00 || next > 0xdfff) return true;
43
+ index += 1;
44
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ async function readCatalogBounded(path: string): Promise<Buffer> {
52
+ const handle = await open(path, "r");
53
+ try {
54
+ const before = await handle.stat();
55
+ if (!before.isFile()) throw new Error("tool catalog must be a regular file");
56
+ if (before.size > CODEBUDDY_TOOL_LIMITS.maxCatalogBytes) {
57
+ throw new Error("tool catalog is too large");
58
+ }
59
+
60
+ // Read at most limit + 1 from the already-open descriptor. The extra byte
61
+ // distinguishes an exact-limit file from a file that grew after fstat,
62
+ // without ever allocating or retaining an attacker-sized input.
63
+ const bytes = Buffer.allocUnsafe(CODEBUDDY_TOOL_LIMITS.maxCatalogBytes + 1);
64
+ let offset = 0;
65
+ while (offset < bytes.length) {
66
+ const result = await handle.read(bytes, offset, bytes.length - offset, offset);
67
+ if (result.bytesRead === 0) break;
68
+ offset += result.bytesRead;
69
+ }
70
+ if (offset > CODEBUDDY_TOOL_LIMITS.maxCatalogBytes) {
71
+ throw new Error("tool catalog is too large");
72
+ }
73
+
74
+ const after = await handle.stat();
75
+ if (
76
+ before.dev !== after.dev
77
+ || before.ino !== after.ino
78
+ || before.size !== after.size
79
+ || before.mtimeMs !== after.mtimeMs
80
+ || before.ctimeMs !== after.ctimeMs
81
+ || after.size !== offset
82
+ ) {
83
+ throw new Error("tool catalog changed while being read");
84
+ }
85
+ return bytes.subarray(0, offset);
86
+ } finally {
87
+ await handle.close();
88
+ }
89
+ }
90
+
91
+ function assertBoundedSchema(schema: Record<string, unknown>): void {
92
+ if (schema.type !== "object") throw new Error("tool input schema must have object type");
93
+ if (utf8Bytes(JSON.stringify(schema)) > CODEBUDDY_TOOL_LIMITS.maxSchemaBytes) {
94
+ throw new Error("tool input schema is too large");
95
+ }
96
+
97
+ let nodes = 0;
98
+ const pending: Array<{ depth: number; value: unknown }> = [{ depth: 0, value: schema }];
99
+ while (pending.length > 0) {
100
+ const current = pending.pop()!;
101
+ nodes += 1;
102
+ if (nodes > CODEBUDDY_TOOL_LIMITS.maxSchemaNodes) {
103
+ throw new Error("tool input schema has too many nodes");
104
+ }
105
+ if (current.depth > CODEBUDDY_TOOL_LIMITS.maxSchemaDepth) {
106
+ throw new Error("tool input schema is too deeply nested");
107
+ }
108
+ if (Array.isArray(current.value)) {
109
+ for (const child of current.value) pending.push({ depth: current.depth + 1, value: child });
110
+ } else if (isRecord(current.value)) {
111
+ for (const child of Object.values(current.value)) {
112
+ pending.push({ depth: current.depth + 1, value: child });
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ async function loadTools(path: string): Promise<ToolDefinition[]> {
119
+ const bytes = await readCatalogBounded(path);
120
+ const parsed: unknown = JSON.parse(bytes.toString("utf8"));
121
+ if (!Array.isArray(parsed)) throw new Error("tool catalog must be an array");
122
+ if (parsed.length > CODEBUDDY_TOOL_LIMITS.maxTools) {
123
+ throw new Error("tool catalog contains too many definitions");
124
+ }
125
+
126
+ const names = new Set<string>();
127
+ return parsed.map((value): ToolDefinition => {
128
+ if (
129
+ !isRecord(value)
130
+ || typeof value.name !== "string"
131
+ || !MCP_TOOL_NAME_PATTERN.test(value.name)
132
+ || utf8Bytes(value.name) > CODEBUDDY_TOOL_LIMITS.maxNameBytes
133
+ || typeof value.description !== "string"
134
+ || value.description.length < 1
135
+ || hasUnpairedSurrogate(value.description)
136
+ || INVALID_DESCRIPTION_CONTROL_PATTERN.test(value.description)
137
+ || utf8Bytes(value.description) > CODEBUDDY_TOOL_LIMITS.maxDescriptionBytes
138
+ || !isRecord(value.inputSchema)
139
+ ) {
140
+ throw new Error("tool catalog contains an invalid definition");
141
+ }
142
+ if (names.has(value.name)) throw new Error("tool catalog contains duplicate names");
143
+ names.add(value.name);
144
+ assertBoundedSchema(value.inputSchema);
145
+ const definition = {
146
+ name: value.name,
147
+ description: value.description,
148
+ inputSchema: value.inputSchema,
149
+ };
150
+ if (utf8Bytes(JSON.stringify(definition)) > CODEBUDDY_TOOL_LIMITS.maxToolBytes) {
151
+ throw new Error("tool catalog contains an oversized definition");
152
+ }
153
+ return definition;
154
+ });
155
+ }
156
+
157
+ export async function runCodeBuddyMcpServer(catalogPath: string): Promise<void> {
158
+ if (!catalogPath) throw new Error("missing tool catalog");
159
+ // The pinned SDK does not detect stdin EOF itself. The capture server must exit when
160
+ // the parent terminates its CLI, including after a captured message_stop.
161
+ const exitOnStdinClose = (): void => process.exit(0);
162
+ process.stdin.on("end", exitOnStdinClose);
163
+ process.stdin.on("close", exitOnStdinClose);
164
+
165
+ const tools = await loadTools(catalogPath);
166
+ const advertisedNames = new Set(tools.map(tool => tool.name));
167
+ const server = new Server(
168
+ { name: "opencodex-codebuddy-capture", version: "1.0.0" },
169
+ { capabilities: { tools: {} } },
170
+ );
171
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
172
+ server.setRequestHandler(CallToolRequestSchema, async request => {
173
+ if (!advertisedNames.has(request.params.name)) throw new Error("unknown isolated tool");
174
+ // The external Codex client retains approval and execution ownership.
175
+ return await new Promise<never>(() => {});
176
+ });
177
+ await server.connect(new StdioServerTransport());
178
+ }
179
+
180
+ if (import.meta.main) await runCodeBuddyMcpServer(process.argv[2] ?? "");
@@ -28,9 +28,28 @@ interface ScanResult {
28
28
  lineStart: boolean;
29
29
  }
30
30
 
31
+ // Probe with ASCII-only case folding rather than toLowerCase(): Unicode lowercasing can expand
32
+ // a code point (İ, ʼn, ligatures), shifting folded-text offsets away from `text` positions and
33
+ // silently disabling detection. `expected` must already be lowercase.
34
+ function asciiFold(code: number): number {
35
+ return code >= 0x41 && code <= 0x5a ? code + 0x20 : code;
36
+ }
37
+
38
+ function startsWithFolded(text: string, index: number, expected: string): boolean {
39
+ if (index + expected.length > text.length) return false;
40
+ for (let i = 0; i < expected.length; i++) {
41
+ if (asciiFold(text.charCodeAt(index + i)) !== expected.charCodeAt(i)) return false;
42
+ }
43
+ return true;
44
+ }
45
+
31
46
  function prefixAtEnd(text: string, at: number, expected: string): boolean {
32
- const rest = text.slice(at).toLowerCase();
33
- return rest.length < expected.length && expected.startsWith(rest);
47
+ const remaining = text.length - at;
48
+ if (remaining > expected.length) return false;
49
+ for (let i = 0; i < remaining; i++) {
50
+ if (asciiFold(text.charCodeAt(at + i)) !== expected.charCodeAt(i)) return false;
51
+ }
52
+ return true;
34
53
  }
35
54
 
36
55
  /**
@@ -65,8 +84,7 @@ function scan(
65
84
  }
66
85
 
67
86
  if (!fence) {
68
- const lowered = text.slice(index).toLowerCase();
69
- if (lowered.startsWith(DSML_CALLS_LINE)) {
87
+ if (startsWithFolded(text, index, DSML_CALLS_LINE)) {
70
88
  const afterCalls = index + DSML_CALLS_LINE.length;
71
89
  let invokeAt = -1;
72
90
  if (text[afterCalls] === "\n") invokeAt = afterCalls + 1;
@@ -76,12 +94,11 @@ function scan(
76
94
  }
77
95
 
78
96
  if (invokeAt >= 0) {
79
- const invokeRest = text.slice(invokeAt).toLowerCase();
80
- const invokeNameStart = invokeRest[DSML_INVOKE_PREFIX.length];
81
- if (invokeRest.startsWith(DSML_INVOKE_PREFIX) && invokeNameStart && !/[\s"]/.test(invokeNameStart)) {
97
+ const invokeNameStart = text[invokeAt + DSML_INVOKE_PREFIX.length];
98
+ if (startsWithFolded(text, invokeAt, DSML_INVOKE_PREFIX) && invokeNameStart && !/[\s"]/.test(invokeNameStart)) {
82
99
  return { safe: text.slice(0, index), held: "", fail: true, fence, lineStart };
83
100
  }
84
- if (invokeRest.length === 0 || DSML_INVOKE_PREFIX.startsWith(invokeRest)) {
101
+ if (invokeAt === text.length || prefixAtEnd(text, invokeAt, DSML_INVOKE_PREFIX)) {
85
102
  return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
86
103
  }
87
104
  }