@centerforagenticai/pi-multi-account 0.1.1 → 0.1.4

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/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.1",
2
+ "version": "0.1.4",
3
3
  "description": "Global Anthropic and OpenAI Codex multi-account OAuth routing for Pi.",
4
4
  "type": "module",
5
5
  "bin": {
@@ -24,8 +24,9 @@
24
24
  * Provenance: pi-anthropic-oauth@0.2.4-intel.2, private pin
25
25
  * 99ac00f290efbcb93e7f23e6b7482d8727367aa7, upstream baseline
26
26
  * 53266ecb51b6d1890ef3f7251a64cb1d71d96099, source src/stream.ts.
27
- * Local delta: adaptive thinking emits type=adaptive and mapped output effort,
28
- * without budget_tokens; all other request and response behavior is unchanged.
27
+ * Local delta: adaptive thinking emits type=adaptive and mapped output effort
28
+ * without budget_tokens; request-local retry/timeout options reach the SDK; and
29
+ * refusal or unknown stop reasons retain bounded, structured error details.
29
30
  */
30
31
 
31
32
  import { Anthropic } from "@anthropic-ai/sdk";
@@ -41,6 +42,8 @@ import {
41
42
  type SimpleStreamOptions,
42
43
  type StopReason,
43
44
  } from "@earendil-works/pi-ai";
45
+ import { sanitizeDiagnosticText } from "./diagnostics.js";
46
+ import type { ProviderErrorCode } from "./error-classification.js";
44
47
  type IndexedBlock =
45
48
  | ({ type: "text"; text: string } & { index: number })
46
49
  | ({ type: "thinking"; thinking: string; thinkingSignature?: string } & {
@@ -95,18 +98,51 @@ const REQUIRED_BETAS = [
95
98
  "interleaved-thinking-2025-05-14",
96
99
  ] as const;
97
100
 
98
- function mapStopReason(reason: string | null | undefined): StopReason {
101
+ type StopReasonResult = Readonly<{
102
+ stopReason: StopReason;
103
+ errorMessage?: string;
104
+ code?: Extract<ProviderErrorCode, "refusal" | "unknown_stop">;
105
+ }>;
106
+
107
+ function mapStopReason(
108
+ reason: string | null | undefined,
109
+ stopDetails?: unknown,
110
+ ): StopReasonResult {
99
111
  switch (reason) {
100
112
  case "end_turn":
101
113
  case "pause_turn":
102
114
  case "stop_sequence":
103
- return "stop";
115
+ return { stopReason: "stop" };
104
116
  case "max_tokens":
105
- return "length";
117
+ return { stopReason: "length" };
106
118
  case "tool_use":
107
- return "toolUse";
108
- default:
109
- return "error";
119
+ return { stopReason: "toolUse" };
120
+ case "refusal": {
121
+ const explanation =
122
+ typeof stopDetails === "object" &&
123
+ stopDetails !== null &&
124
+ typeof (stopDetails as { explanation?: unknown }).explanation === "string"
125
+ ? sanitizeDiagnosticText(
126
+ (stopDetails as { explanation: string }).explanation,
127
+ )
128
+ : "";
129
+ return {
130
+ stopReason: "error",
131
+ errorMessage:
132
+ explanation || "The model refused to complete the request",
133
+ code: "refusal",
134
+ };
135
+ }
136
+ default: {
137
+ const boundedReason = sanitizeDiagnosticText(reason ?? "unknown");
138
+ return {
139
+ stopReason: "error",
140
+ errorMessage: sanitizeDiagnosticText(
141
+ `Provider stopped with: ${boundedReason}`,
142
+ ),
143
+ code: "unknown_stop",
144
+ };
145
+ }
110
146
  }
111
147
  }
112
148
 
@@ -163,7 +199,7 @@ export function streamAnthropicAdaptive(
163
199
  const stream = createAssistantMessageEventStream();
164
200
 
165
201
  void (async () => {
166
- const output: AssistantMessage = {
202
+ const output: AssistantMessage & { code?: ProviderErrorCode } = {
167
203
  role: "assistant",
168
204
  content: [],
169
205
  api: model.api,
@@ -276,10 +312,24 @@ export function streamAnthropicAdaptive(
276
312
  // which throws under fine-grained-tool-streaming (input may be invalid
277
313
  // mid-flight) and aborts the turn. The raw stream yields the same
278
314
  // RawMessageStreamEvents; tool args are already parsed leniently below.
315
+ const maxRetries = options?.maxRetries;
316
+ const timeoutMs = options?.timeoutMs;
279
317
  const { data: anthropicStream, response: httpResponse } =
280
318
  await client.messages
281
319
  .create(params, {
282
320
  signal: options?.signal,
321
+ ...(Number.isFinite(maxRetries) &&
322
+ Number.isInteger(maxRetries) &&
323
+ (maxRetries ?? -1) >= 0
324
+ ? { maxRetries }
325
+ : {}),
326
+ // Any finite positive timeout is honored; the SDK timer takes whole
327
+ // milliseconds, so a fractional value is floored to at least 1.
328
+ ...(typeof timeoutMs === "number" &&
329
+ Number.isFinite(timeoutMs) &&
330
+ timeoutMs > 0
331
+ ? { timeout: Math.max(1, Math.floor(timeoutMs)) }
332
+ : {}),
283
333
  })
284
334
  .withResponse();
285
335
 
@@ -474,7 +524,19 @@ export function streamAnthropicAdaptive(
474
524
  }
475
525
 
476
526
  if (event.type === "message_delta") {
477
- output.stopReason = mapStopReason(event.delta.stop_reason);
527
+ const rawStopReason = event.delta.stop_reason;
528
+ const mapped = mapStopReason(
529
+ rawStopReason,
530
+ (event.delta as { stop_details?: unknown }).stop_details,
531
+ );
532
+ output.stopReason = mapped.stopReason;
533
+ if (typeof rawStopReason === "string") {
534
+ output.rawStopReason = sanitizeDiagnosticText(rawStopReason);
535
+ }
536
+ if (mapped.errorMessage !== undefined) {
537
+ output.errorMessage = mapped.errorMessage;
538
+ }
539
+ if (mapped.code !== undefined) output.code = mapped.code;
478
540
  output.usage.input =
479
541
  (event.usage as { input_tokens?: number }).input_tokens ||
480
542
  output.usage.input;
@@ -505,11 +567,15 @@ export function streamAnthropicAdaptive(
505
567
  }
506
568
 
507
569
  if (options?.signal?.aborted) throw new Error("Request aborted");
508
- stream.push({
509
- type: "done",
510
- reason: output.stopReason as "stop" | "length" | "toolUse",
511
- message: output,
512
- });
570
+ if (output.stopReason === "error") {
571
+ stream.push({ type: "error", reason: "error", error: output });
572
+ } else {
573
+ stream.push({
574
+ type: "done",
575
+ reason: output.stopReason as "stop" | "length" | "toolUse",
576
+ message: output,
577
+ });
578
+ }
513
579
  stream.end();
514
580
  } catch (error) {
515
581
  for (const block of output.content as Array<{
@@ -13,6 +13,7 @@ import {
13
13
  sanitizeDiagnosticText,
14
14
  sanitizeHeaderValue,
15
15
  } from "./diagnostics.js";
16
+ import { hostFinalStopMessage } from "./host-final-stop-message.js";
16
17
 
17
18
  export const ANTHROPIC_ALIAS_API = "hypha-anthropic-oauth" as const;
18
19
 
@@ -53,12 +54,19 @@ function withAliasAttribution(
53
54
  };
54
55
  }
55
56
 
57
+ /**
58
+ * Bounds an upstream error and, for a structured refusal or unknown stop,
59
+ * publishes the shared host-final message. A direct alias turn reaches the
60
+ * host's retry and compaction predicates without the unified provider, so
61
+ * provider-authored stop wording must not make the host resend the request.
62
+ */
56
63
  function sanitizeUpstreamError(message: AssistantMessage): AssistantMessage {
57
64
  if (message.errorMessage === undefined) return message;
58
- return {
65
+ const sanitized: AssistantMessage = {
59
66
  ...message,
60
67
  errorMessage: sanitizeDiagnosticText(message.errorMessage),
61
68
  };
69
+ return { ...sanitized, ...hostFinalStopMessage(sanitized) };
62
70
  }
63
71
 
64
72
  function withAliasEvent(
@@ -235,9 +235,33 @@ function normalizeAliasContext(
235
235
  }
236
236
 
237
237
  /**
238
- * Wraps the exact maintained stream without replacing transport or OAuth. Pi's
238
+ * Temporary containment for the Pi 0.99 Codex WebSocket failure: a WebSocket
239
+ * error leaves the session in a state where the next Codex turn crashes with
240
+ * "Cannot read properties of undefined (reading 'length')". Routes this
241
+ * extension owns always request SSE. The base `openai-codex` provider is never
242
+ * touched; only options on calls we already route are adjusted. Remove once the
243
+ * WebSocket path is fixed upstream.
244
+ */
245
+ export const CODEX_FORCED_TRANSPORT = "sse" as const;
246
+
247
+ /** Returns options with Codex transport pinned to SSE, and whether a value was overridden. */
248
+ export function forceCodexSseOptions<T extends SimpleStreamOptions>(
249
+ options: T | undefined,
250
+ ): { options: T; overridden: boolean } {
251
+ if (options?.transport === CODEX_FORCED_TRANSPORT) {
252
+ return { options, overridden: false };
253
+ }
254
+ return {
255
+ options: { ...(options ?? {}), transport: CODEX_FORCED_TRANSPORT } as T,
256
+ overridden: true,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Wraps the exact maintained stream without replacing OAuth. Pi's
239
262
  * alias-resolved apiKey and every other option field are forwarded unchanged;
240
- * only model/context identity and callback attribution are adapted.
263
+ * model/context identity and callback attribution are adapted, and transport
264
+ * is pinned to SSE (see {@link CODEX_FORCED_TRANSPORT}).
241
265
  */
242
266
  export function createCodexAliasStream(
243
267
  maintainedStream: NonNullable<ProviderConfig["streamSimple"]>,
@@ -251,12 +275,12 @@ export function createCodexAliasStream(
251
275
  };
252
276
  const upstreamContext = normalizeAliasContext(context, aliasModel);
253
277
 
254
- let upstreamOptions = options;
278
+ let upstreamOptions = forceCodexSseOptions(options).options;
255
279
  if (options?.onPayload || options?.onResponse) {
256
280
  const aliasOnPayload = options.onPayload;
257
281
  const aliasOnResponse = options.onResponse;
258
282
  upstreamOptions = {
259
- ...options,
283
+ ...upstreamOptions,
260
284
  ...(aliasOnPayload
261
285
  ? {
262
286
  onPayload: (payload: unknown) =>
@@ -13,6 +13,8 @@ export const PROVIDER_ERROR_CODES = [
13
13
  "model_not_found",
14
14
  "unsupported_api_version",
15
15
  "invalid_request",
16
+ "refusal",
17
+ "unknown_stop",
16
18
  ] as const;
17
19
  export type ProviderErrorCode = (typeof PROVIDER_ERROR_CODES)[number];
18
20
 
@@ -50,10 +52,12 @@ export type FailureCategory =
50
52
 
51
53
  export interface FailureClassification {
52
54
  readonly category: FailureCategory;
55
+ readonly kind?: "refusal" | "unknown-stop";
53
56
  readonly accountAction:
54
57
  | "cooldown-and-route"
55
58
  | "invalidate-and-route"
56
- | "route-without-retry";
59
+ | "route-without-retry"
60
+ | "retain-account";
57
61
  readonly cooldownReason?: CooldownReason;
58
62
  readonly serverHint?: NumericServerHint;
59
63
  }
@@ -248,6 +252,14 @@ export function classifyFailure(
248
252
  return { category: "config", accountAction: "route-without-retry" };
249
253
  }
250
254
 
255
+ if (signal.code === "refusal" || signal.code === "unknown_stop") {
256
+ return {
257
+ category: "unknown",
258
+ kind: signal.code === "refusal" ? "refusal" : "unknown-stop",
259
+ accountAction: "retain-account",
260
+ };
261
+ }
262
+
251
263
  if (isTransportFailure(signal)) {
252
264
  return cooldown("transport", "transport", signal);
253
265
  }
@@ -0,0 +1,124 @@
1
+ import {
2
+ isContextOverflow,
3
+ isRetryableAssistantError,
4
+ type AssistantMessage,
5
+ } from "@earendil-works/pi-ai";
6
+ import { sanitizeDiagnosticText } from "./diagnostics.js";
7
+
8
+ /** Fixed fallbacks for a structured provider stop; none matches a host re-dispatch pattern. */
9
+ export const REFUSAL_FALLBACK_MESSAGE = "The model refused to complete the request";
10
+ export const UNKNOWN_STOP_FALLBACK_MESSAGE =
11
+ "Provider stopped with an unrecognized stop reason";
12
+
13
+ /** Upper bound on the normalized stop reason named in a reworded message. */
14
+ const MAX_NAMED_REASON_LENGTH = 64;
15
+
16
+ /**
17
+ * Whether the pinned host would re-dispatch an error terminal carrying `text`.
18
+ *
19
+ * The host re-dispatches when either retry predicate matches the prose:
20
+ * `AgentSession._isRetryableError` runs pi-ai `isRetryableAssistantError`, and
21
+ * `_checkCompaction` runs `isContextOverflow` before compacting and retrying once
22
+ * (pi-coding-agent dist/core/agent-session.js, both from `@earendil-works/pi-ai`).
23
+ * An unreadable predicate result counts as a match, so the caller keeps looking
24
+ * for a safer form.
25
+ */
26
+ function hostWouldRedispatch(message: AssistantMessage, text: string): boolean {
27
+ try {
28
+ const probe = { ...message, errorMessage: text };
29
+ return isRetryableAssistantError(probe) || isContextOverflow(probe, 0);
30
+ } catch {
31
+ return true;
32
+ }
33
+ }
34
+
35
+ function namedReasonMessage(reason: string): string {
36
+ return `Provider stopped (reason: ${reason})`;
37
+ }
38
+
39
+ /**
40
+ * Normalizes a bounded, sanitized raw stop reason into plain words: every run of
41
+ * non-alphanumeric characters (underscores included) becomes one space, and the
42
+ * result is capped at a word boundary where possible.
43
+ */
44
+ function normalizedReasonWords(rawStopReason: unknown): string[] {
45
+ if (typeof rawStopReason !== "string") return [];
46
+ const words = sanitizeDiagnosticText(rawStopReason)
47
+ .replace(/[^A-Za-z0-9]+/g, " ")
48
+ .trim()
49
+ .split(" ")
50
+ .filter((word) => word.length > 0);
51
+ const kept: string[] = [];
52
+ let length = 0;
53
+ for (const word of words) {
54
+ const next = length === 0 ? word.length : length + 1 + word.length;
55
+ if (next > MAX_NAMED_REASON_LENGTH) {
56
+ if (kept.length === 0) kept.push(word.slice(0, MAX_NAMED_REASON_LENGTH));
57
+ break;
58
+ }
59
+ kept.push(word);
60
+ length = next;
61
+ }
62
+ return kept;
63
+ }
64
+
65
+ /**
66
+ * Names an unknown stop reason in a message neither host predicate acts on.
67
+ *
68
+ * The first form keeps every normalized word. When that still matches a host
69
+ * predicate (a reason containing "overloaded" or "timeout", say), the second
70
+ * form admits words in order and drops each one whose addition would make the
71
+ * message match. Every admitted prefix was probed, so the result is host-final
72
+ * by construction. An empty result yields `undefined`.
73
+ */
74
+ function unknownStopNamingReason(
75
+ message: AssistantMessage,
76
+ rawStopReason: unknown,
77
+ ): string | undefined {
78
+ const words = normalizedReasonWords(rawStopReason);
79
+ if (words.length === 0) return undefined;
80
+ const reworded = namedReasonMessage(words.join(" "));
81
+ if (!hostWouldRedispatch(message, reworded)) return reworded;
82
+ const kept: string[] = [];
83
+ for (const word of words) {
84
+ const tentative = namedReasonMessage([...kept, word].join(" "));
85
+ if (!hostWouldRedispatch(message, tentative)) kept.push(word);
86
+ }
87
+ return kept.length === 0 ? undefined : namedReasonMessage(kept.join(" "));
88
+ }
89
+
90
+ /**
91
+ * The public error text for a structured refusal or unknown provider stop.
92
+ *
93
+ * The pinned host re-dispatches an error terminal from its prose alone (see
94
+ * `hostWouldRedispatch`). A refusal explanation or stop reason is
95
+ * provider-authored, so text such as "overloaded", "prompt is too long", or
96
+ * "model_context_window_exceeded" would make the host send the stopped request
97
+ * again. The structured code alone selects this path; the provider text is kept
98
+ * only when neither host predicate would act on it. An unknown stop is then
99
+ * reworded so it still names its reason, and only when no reworded form is safe
100
+ * is a fixed fallback published. A refusal whose explanation is unsafe publishes
101
+ * its fixed fallback directly. A message without the structured code, or that is
102
+ * not an error terminal, yields `undefined` and must be published unchanged.
103
+ */
104
+ export function hostFinalStopMessage(
105
+ message: AssistantMessage,
106
+ ): { readonly errorMessage: string } | undefined {
107
+ const code = (message as { code?: unknown }).code;
108
+ if (message.stopReason !== "error") return undefined;
109
+ if (code !== "refusal" && code !== "unknown_stop") return undefined;
110
+ const candidate = message.errorMessage;
111
+ if (
112
+ typeof candidate === "string" &&
113
+ candidate.length > 0 &&
114
+ !hostWouldRedispatch(message, candidate)
115
+ ) {
116
+ return { errorMessage: candidate };
117
+ }
118
+ if (code === "refusal") return { errorMessage: REFUSAL_FALLBACK_MESSAGE };
119
+ return {
120
+ errorMessage:
121
+ unknownStopNamingReason(message, message.rawStopReason) ??
122
+ UNKNOWN_STOP_FALLBACK_MESSAGE,
123
+ };
124
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
4
+ import { join } from "node:path";
6
5
  import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
7
6
  import { readStoredCredential } from "@earendil-works/pi-coding-agent";
8
7
  import type {
@@ -29,6 +28,10 @@ import {
29
28
  createOpenAiAliasProviderConfig,
30
29
  openaiPlatformModels,
31
30
  } from "./openai-adapter.js";
31
+ import {
32
+ loadMaintainedCodexStream,
33
+ resolveInstalledPiAiVersion,
34
+ } from "./pi-ai-entrypoints.js";
32
35
  import {
33
36
  ALLOWED_FAMILIES,
34
37
  DEFAULT_CONFIG,
@@ -405,6 +408,16 @@ function readInstalledDeclaration(targetPath: string): unknown {
405
408
  }
406
409
  }
407
410
 
411
+ /** The session's id, or undefined when the host context cannot supply one. */
412
+ function readSessionId(ctx: ExtensionContext): string | undefined {
413
+ try {
414
+ const id = (ctx.sessionManager as { getSessionId?: () => unknown }).getSessionId?.();
415
+ return typeof id === "string" && id.length > 0 ? id : undefined;
416
+ } catch {
417
+ return undefined;
418
+ }
419
+ }
420
+
408
421
  /** The complete declaration-gated startup seam, exported for causal registration contracts. */
409
422
  export function registerLogicalProviderAtMatchedStartup(input: {
410
423
  pi: Parameters<typeof registerLogicalProviderForSession>[0];
@@ -413,6 +426,8 @@ export function registerLogicalProviderAtMatchedStartup(input: {
413
426
  deps: LogicalProviderDeps;
414
427
  attributionStoreFactory?: () => LogicalAttributionLifecycle;
415
428
  registerLogicalApi?: LogicalApiRegistrar;
429
+ /** The registering session's id; routes `unified` calls back to their caller. */
430
+ sessionId?: string | undefined;
416
431
  }): {
417
432
  status: ReturnType<typeof inspectInstalledDeclaration>;
418
433
  expectedModels: ReturnType<typeof buildModelDeclaration>["models"];
@@ -424,6 +439,7 @@ export function registerLogicalProviderAtMatchedStartup(input: {
424
439
  deps,
425
440
  attributionStoreFactory,
426
441
  registerLogicalApi,
442
+ sessionId,
427
443
  } = input;
428
444
  const expected = buildModelDeclaration(liveCatalogs);
429
445
  const status = inspectInstalledDeclaration(installed, expected.models);
@@ -442,6 +458,7 @@ export function registerLogicalProviderAtMatchedStartup(input: {
442
458
  expected.models,
443
459
  attributedDeps,
444
460
  registerLogicalApi,
461
+ sessionId === undefined ? {} : { sessionId },
445
462
  );
446
463
  }
447
464
  return { status, expectedModels: expected.models };
@@ -1242,24 +1259,6 @@ function continuationReason(
1242
1259
  }
1243
1260
  }
1244
1261
 
1245
- async function loadMaintainedCodexStream(): Promise<
1246
- NonNullable<ProviderConfig["streamSimple"]>
1247
- > {
1248
- const publicRoot = await import("@earendil-works/pi-ai");
1249
- const compatibilityStream = (
1250
- publicRoot as unknown as Record<string, unknown>
1251
- )["streamSimpleOpenAICodexResponses"];
1252
- if (typeof compatibilityStream === "function") {
1253
- return compatibilityStream as NonNullable<ProviderConfig["streamSimple"]>;
1254
- }
1255
- const maintainedApi = await import(
1256
- "@earendil-works/pi-ai/api/openai-codex-responses"
1257
- );
1258
- return maintainedApi.streamSimple as unknown as NonNullable<
1259
- ProviderConfig["streamSimple"]
1260
- >;
1261
- }
1262
-
1263
1262
  /**
1264
1263
  * Fail-closed sentinel used only when the host codex OAuth surface is
1265
1264
  * unavailable at session_start (host shape drift). Its OAuth callbacks throw a
@@ -2065,50 +2064,6 @@ function cacheInheritedAccountGroupResolution(
2065
2064
  }
2066
2065
 
2067
2066
 
2068
- /**
2069
- * Resolves the actually-installed `@earendil-works/pi-ai` package version by
2070
- * walking up from its resolved `providers/anthropic.models` module file to
2071
- * the nearest `package.json` whose `name` matches. A local, bounded,
2072
- * offline disk read -- never a network request -- so the report's recorded
2073
- * pricing-method provenance names whichever pinned catalog this process
2074
- * actually loaded instead of a hard-coded string that could drift from it.
2075
- * Shares its shape with the standalone CLI's own offline resolver in
2076
- * `standalone-cli.ts`; the two adapters read from different sources (a live
2077
- * `ExtensionContext.modelRegistry` here, the pinned package directly there)
2078
- * and so cannot share one function without coupling this extension entry
2079
- * point to the separate standalone-CLI module.
2080
- */
2081
- function resolveInstalledPiAiVersion(): string {
2082
- try {
2083
- const moduleUrl = import.meta.resolve(
2084
- "@earendil-works/pi-ai/providers/anthropic.models",
2085
- );
2086
- let directory = dirname(fileURLToPath(moduleUrl));
2087
- for (let depth = 0; depth < 6; depth += 1) {
2088
- try {
2089
- const candidate = JSON.parse(
2090
- readFileSync(join(directory, "package.json"), "utf-8"),
2091
- ) as { readonly name?: unknown; readonly version?: unknown };
2092
- if (
2093
- candidate.name === "@earendil-works/pi-ai" &&
2094
- typeof candidate.version === "string" &&
2095
- candidate.version.length > 0
2096
- ) {
2097
- return `pi-ai@${candidate.version}`;
2098
- }
2099
- } catch {
2100
- // Keep walking toward the installed package root.
2101
- }
2102
- const parent = dirname(directory);
2103
- if (parent === directory) break;
2104
- directory = parent;
2105
- }
2106
- } catch {
2107
- // Resolution failure falls through to the bounded fallback below.
2108
- }
2109
- return "pi-ai@unknown";
2110
- }
2111
-
2112
2067
  /**
2113
2068
  * Builds the extension surfaces' live Pi-catalog snapshot from the session's
2114
2069
  * real `ExtensionContext.modelRegistry.getAll()` -- the same public,
@@ -3935,6 +3890,18 @@ export const createMultiAccountExtension =
3935
3890
  modelSupport,
3936
3891
  alreadyCooled: classified.alreadyCooled === true,
3937
3892
  });
3893
+ if (decision.status === "retained") {
3894
+ // A structured refusal or unknown stop keeps the account: no switch,
3895
+ // no park, no OpenRouter, and no continuation. The same context would
3896
+ // stop the same way on any account.
3897
+ diagnostics.record(
3898
+ "info",
3899
+ "routing.retained",
3900
+ "The provider stopped the turn with a refusal or unknown stop reason; the account was kept and no follow-up was scheduled.",
3901
+ { providerId, kind: decision.classification.kind ?? "unknown" },
3902
+ );
3903
+ return;
3904
+ }
3938
3905
  if (decision.status === "paused") {
3939
3906
  // OpenRouter is an explicitly enabled, metered FINAL rung. The helper
3940
3907
  // re-checks every managed family so family-chain policy cannot bypass
@@ -4531,6 +4498,8 @@ export const createMultiAccountExtension =
4531
4498
  installed,
4532
4499
  liveCatalogs,
4533
4500
  registerLogicalApi,
4501
+ // Optional routing key; an unreadable id must never block registration.
4502
+ sessionId: readSessionId(startupContext),
4534
4503
  attributionStoreFactory: () => {
4535
4504
  const store = createAttributionStore({
4536
4505
  onResponse: (route, response) => {
@@ -4916,7 +4885,7 @@ export const createMultiAccountExtension =
4916
4885
  config,
4917
4886
  nowMs,
4918
4887
  });
4919
- if (reactive.status === "paused") return;
4888
+ if (reactive.status !== "selected") return;
4920
4889
  reactiveCandidate = reactive;
4921
4890
  destinationProviderId = reactive.destination.providerId;
4922
4891
  }
@@ -5342,9 +5311,17 @@ export const createMultiAccountExtension =
5342
5311
  turnRouteOrigin = origin;
5343
5312
  handledFailures.add(event.message);
5344
5313
  const responseFailure = lastFailure.get(providerId) ?? {};
5345
- const messageCode = providerErrorCodeFromMessage(
5346
- event.message.errorMessage,
5347
- );
5314
+ // A structured provider stop is copied only from this two-value allowlist,
5315
+ // never spread, and it wins over any code parsed from provider-authored
5316
+ // errorMessage prose: refusal text must not read as a rate limit.
5317
+ const rawStopCode = (event.message as { code?: unknown }).code;
5318
+ const stopCode =
5319
+ rawStopCode === "refusal" || rawStopCode === "unknown_stop"
5320
+ ? rawStopCode
5321
+ : undefined;
5322
+ const messageCode =
5323
+ stopCode ??
5324
+ providerErrorCodeFromMessage(event.message.errorMessage);
5348
5325
  const failure: ProviderFailureSignal = {
5349
5326
  ...responseFailure,
5350
5327
  ...(messageCode === undefined ? {} : { code: messageCode }),
@@ -22,6 +22,7 @@ import {
22
22
  type SimpleStreamOptions,
23
23
  } from "@earendil-works/pi-ai";
24
24
  import { RuntimeState, type LogicalRoutePin } from "./runtime-state.js";
25
+ import { hostFinalStopMessage } from "./host-final-stop-message.js";
25
26
  import { DEFAULT_CONFIG } from "./config.js";
26
27
  import type {
27
28
  AllowedFamily,
@@ -47,6 +48,16 @@ import type { ProviderType, Vendor } from "./vendor.js";
47
48
 
48
49
  export { LOGICAL_PROVIDER_ID } from "./models-declaration.js";
49
50
  import { LOGICAL_PROVIDER_ID } from "./models-declaration.js";
51
+ import { forceCodexSseOptions } from "./codex-adapter.js";
52
+
53
+ /** Fixed diagnostic label for a caller-supplied transport; never echoes the raw value. */
54
+ function codexTransportLabel(
55
+ transport: unknown,
56
+ ): "websocket" | "websocket-cached" | "auto" | "other" {
57
+ return transport === "websocket" || transport === "websocket-cached" || transport === "auto"
58
+ ? transport
59
+ : "other";
60
+ }
50
61
 
51
62
  /** One physical account the logical provider may dispatch to. */
52
63
  export interface LogicalPhysicalAccount {
@@ -666,6 +677,7 @@ export function createLogicalProvider(
666
677
  deps: LogicalProviderDeps,
667
678
  ): LogicalProvider {
668
679
  const coordinator = createHostRetryCoordinator(deps);
680
+ let codexTransportNoticeSent = false;
669
681
 
670
682
  const diagnose = (message: string): void => {
671
683
  deps.onDiagnostic?.(message);
@@ -936,6 +948,7 @@ export function createLogicalProvider(
936
948
 
937
949
  const projectMessage = (message: AssistantMessage, modelId: string): AssistantMessage => ({
938
950
  ...message, api: LOGICAL_PROVIDER_ID, provider: LOGICAL_PROVIDER_ID, model: modelId,
951
+ ...hostFinalStopMessage(message),
939
952
  });
940
953
 
941
954
  const projectEvent = (event: unknown, modelId: string): unknown => {
@@ -1127,8 +1140,24 @@ export function createLogicalProvider(
1127
1140
  await originalOnResponse(response, responseModel);
1128
1141
  }
1129
1142
  };
1143
+ let routedOptions = options;
1144
+ if (account.family === "openai-codex") {
1145
+ // Temporary Pi 0.99 WebSocket containment; see CODEX_FORCED_TRANSPORT.
1146
+ const forced = forceCodexSseOptions(options);
1147
+ routedOptions = forced.options;
1148
+ if (forced.overridden && options?.transport !== undefined && !codexTransportNoticeSent) {
1149
+ codexTransportNoticeSent = true;
1150
+ try {
1151
+ deps.onDiagnostic?.(
1152
+ `Codex transport "${codexTransportLabel(options.transport)}" overridden to "sse" on routed calls (Pi 0.99 WebSocket containment).`,
1153
+ );
1154
+ } catch {
1155
+ // A diagnostic sink failure cannot replace a provider result.
1156
+ }
1157
+ }
1158
+ }
1130
1159
  const attributedOptions: SimpleStreamOptions = {
1131
- ...options,
1160
+ ...routedOptions,
1132
1161
  onPayload: wrappedOnPayload,
1133
1162
  onResponse: wrappedOnResponse,
1134
1163
  };