@selesai/code 0.13.2 → 0.13.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/defaults/models.json +85 -13
  3. package/dist/extensions/cost-reconcile.test.ts +200 -4
  4. package/dist/extensions/cost-reconcile.ts +88 -102
  5. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  6. package/dist/extensions/pi-intercom/README.md +4 -5
  7. package/dist/extensions/pi-intercom/config.test.ts +3 -31
  8. package/dist/extensions/pi-intercom/config.ts +0 -15
  9. package/dist/extensions/pi-intercom/index.ts +9 -46
  10. package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
  11. package/dist/extensions/pi-intercom/package.json +1 -1
  12. package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
  13. package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
  14. package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
  15. package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
  16. package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
  17. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  18. package/dist/extensions/pi-subagents/package.json +1 -1
  19. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
  20. package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
  21. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
  22. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  23. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
  24. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  25. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  27. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
  28. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
  29. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
  30. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  31. package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
  32. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
  37. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
  39. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
  40. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
  41. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
  42. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
  43. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
  44. package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
  45. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  46. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
  47. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
  48. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
  49. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
  50. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
  51. package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
  52. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
  53. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
  54. package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
  55. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
  56. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
  57. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
  58. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
  59. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
  60. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
  61. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
  62. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
  63. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
  64. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
  65. package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
  66. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
  68. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
  69. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
  70. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
  71. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
  72. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
  73. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
  74. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
  75. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
  76. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
  77. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
  78. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
  79. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
  80. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
  81. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
  82. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
  83. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
  84. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
  85. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
  86. package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
  87. package/dist/extensions/pi-web-agent/README.md +131 -0
  88. package/dist/extensions/pi-web-agent/package.json +4 -2
  89. package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
  90. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  91. package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
  92. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
  93. package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
  94. package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
  95. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
  96. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
  97. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
  98. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
  99. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
  100. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
  101. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
  102. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
  103. package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
  104. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
  105. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
  106. package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
  107. package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
  108. package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
  109. package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
  110. package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
  111. package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
  112. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
  113. package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
  114. package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
  115. package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
  116. package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
  117. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
  118. package/dist/extensions/pi-web-agent/src/types.ts +19 -4
  119. package/dist/extensions/tokenin-onboarding.ts +302 -0
  120. package/package.json +1 -1
@@ -16,20 +16,33 @@ export type SearchResult = {
16
16
  snippet: string;
17
17
  };
18
18
 
19
+ export type SearchProviderName = 'duckduckgo' | 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
20
+
21
+ export type FanoutMode = 'off' | 'on' | 'auto';
22
+
23
+ export type FanoutMetadata = {
24
+ mode: Exclude<FanoutMode, 'off'>;
25
+ providers: SearchProviderName[]; // providers that actually contributed results
26
+ skipped?: SearchProviderName[]; // providers queried but errored or returned nothing
27
+ };
28
+
19
29
  export type ToolError = {
20
30
  code: string;
21
31
  message: string;
22
32
  };
23
33
 
24
34
  export type SearchMetadata = {
25
- backend: 'duckduckgo' | 'searxng' | 'brave';
35
+ backend: 'duckduckgo' | 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
26
36
  cacheHit: boolean;
27
- fallbackFrom?: 'searxng' | 'brave';
37
+ fallbackFrom?: 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily' | 'duckduckgo';
28
38
  fallbackReason?: string;
39
+ fanout?: FanoutMetadata;
29
40
  };
30
41
 
42
+ export type FetchMethod = 'http' | 'headless' | 'firecrawl' | 'github' | 'pdf' | 'youtube';
43
+
31
44
  export type FetchMetadata = {
32
- method: 'http' | 'headless' | 'firecrawl';
45
+ method: FetchMethod;
33
46
  cacheHit: boolean;
34
47
  fallbackFrom?: 'firecrawl';
35
48
  fallbackReason?: string;
@@ -74,7 +87,7 @@ export type WebFetchHeadlessResponse = {
74
87
  export type WebExploreResponse = {
75
88
  status: 'ok' | 'error';
76
89
  findings: string[];
77
- sources: Array<{ title: string; url: string; method?: 'http' | 'headless' | 'firecrawl' }>;
90
+ sources: Array<{ title: string; url: string; method?: FetchMethod }>;
78
91
  caveat?: string;
79
92
  metadata?: {
80
93
  searchPasses: number;
@@ -82,6 +95,8 @@ export type WebExploreResponse = {
82
95
  headlessAttempts: number;
83
96
  exhaustedBudget: boolean;
84
97
  caveatReasons?: string[];
98
+ fanoutProviders?: SearchProviderName[];
99
+ fanoutSkipped?: SearchProviderName[];
85
100
  };
86
101
  presentation?: PresentationEnvelope;
87
102
  error?: ToolError;
@@ -13,6 +13,16 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
13
13
  import { dirname, join } from "node:path";
14
14
  import { getAgentDir, getModelsPath } from "@selesai/code";
15
15
  import type { AuthStorage, ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionStartEvent } from "@selesai/code";
16
+ import {
17
+ createAssistantMessageEventStream,
18
+ lazyStream,
19
+ type AssistantMessageEvent,
20
+ type AssistantMessageEventStream,
21
+ type Context,
22
+ type Model,
23
+ type SimpleStreamOptions,
24
+ } from "@earendil-works/pi-ai";
25
+ import { getApiProvider } from "@earendil-works/pi-ai/compat";
16
26
 
17
27
  export const TOKEN_IN_PROVIDER = "tokenin";
18
28
  export const TOKEN_IN_DASHBOARD_URL = "https://token.selesai.in/dashboard/tokens";
@@ -216,6 +226,291 @@ function isOnboardingComplete(agentDir: string = getAgentDir()): boolean {
216
226
  return existsSync(getOnboardingMarkerPath(agentDir));
217
227
  }
218
228
 
229
+ // ---------------------------------------------------------------------------
230
+ // Multi-key load balancing / auto-failover
231
+ // ---------------------------------------------------------------------------
232
+
233
+ export const DEFAULT_TOKENIN_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
234
+
235
+ /** In-memory cooldown tracker mapping token/accountId -> cooldown expires timestamp (ms). */
236
+ const tokenCooldowns = new Map<string, number>();
237
+
238
+ /** Clear all in-memory cooldowns. Exported for tests. */
239
+ export function clearTokenInCooldowns(): void {
240
+ tokenCooldowns.clear();
241
+ }
242
+
243
+ /** Check if a token/account is currently on cooldown. */
244
+ export function isTokenInOnCooldown(tokenId: string, now: number = Date.now()): boolean {
245
+ const expires = tokenCooldowns.get(tokenId);
246
+ if (expires === undefined) return false;
247
+ if (now >= expires) {
248
+ tokenCooldowns.delete(tokenId);
249
+ return false;
250
+ }
251
+ return true;
252
+ }
253
+
254
+ /** Put a token/account on cooldown for a given duration. */
255
+ export function setTokenInCooldown(tokenId: string, durationMs: number = DEFAULT_TOKENIN_COOLDOWN_MS, now: number = Date.now()): void {
256
+ tokenCooldowns.set(tokenId, now + durationMs);
257
+ }
258
+
259
+ /** Detect if an error message or AssistantMessage stop indicates an auth/quota failure eligible for failover. */
260
+ export function isRotateableTokenInError(error: unknown): boolean {
261
+ if (!error) return false;
262
+ let text = "";
263
+ if (typeof error === "string") {
264
+ text = error;
265
+ } else if (error instanceof Error) {
266
+ text = `${error.name}: ${error.message}`;
267
+ } else if (typeof error === "object" && error !== null) {
268
+ const obj = error as Record<string, unknown>;
269
+ if (typeof obj.errorMessage === "string") {
270
+ text = obj.errorMessage;
271
+ } else if (typeof obj.message === "string") {
272
+ text = obj.message;
273
+ } else if (Array.isArray(obj.content)) {
274
+ // AssistantMessage with content blocks containing error text or errorMessage
275
+ for (const block of obj.content) {
276
+ if (typeof block === "object" && block !== null) {
277
+ const b = block as Record<string, unknown>;
278
+ if (typeof b.errorMessage === "string") text += ` ${b.errorMessage}`;
279
+ if (typeof b.text === "string") text += ` ${b.text}`;
280
+ }
281
+ }
282
+ }
283
+ if (!text) text = JSON.stringify(error);
284
+ } else {
285
+ text = JSON.stringify(error);
286
+ }
287
+
288
+ const lower = text.toLowerCase();
289
+ // HTTP 401 / Invalid Key
290
+ if (lower.includes("401") || lower.includes("unauthorized") || lower.includes("invalid_api_key") || lower.includes("invalid api key") || lower.includes("authentication_error")) {
291
+ return true;
292
+ }
293
+ // HTTP 429 / Rate limit
294
+ if (lower.includes("429") || lower.includes("rate_limit") || lower.includes("rate limit") || lower.includes("too many requests")) {
295
+ return true;
296
+ }
297
+ // Budget / Quota / Credits
298
+ if (
299
+ lower.includes("budget has been exceeded") ||
300
+ lower.includes("budget_exceeded") ||
301
+ lower.includes("max_budget") ||
302
+ lower.includes("insufficient_quota") ||
303
+ lower.includes("quota exceeded") ||
304
+ lower.includes("exceeds max_budget") ||
305
+ lower.includes("credit limit") ||
306
+ lower.includes("out of credits")
307
+ ) {
308
+ return true;
309
+ }
310
+ return false;
311
+ }
312
+
313
+ /** Helper to persist active credential to auth.json and tokenin-auth.json without needing AuthStorage instance. */
314
+ function persistActiveTokenInAccount(account: TokenInAccount, authPath: string = getTokenInAuthPath()): void {
315
+ try {
316
+ // Update tokenin-auth.json
317
+ const auth = readTokenInAuth(authPath);
318
+ auth.activeId = account.id;
319
+ writeTokenInAuth(auth, authPath);
320
+
321
+ // Update auth.json
322
+ const agentDir = dirname(authPath);
323
+ const globalAuthPath = join(agentDir, "auth.json");
324
+ const current = existsSync(globalAuthPath) ? JSON.parse(readFileSync(globalAuthPath, "utf-8")) as Record<string, unknown> : {};
325
+ current[TOKEN_IN_PROVIDER] = { type: "api_key", key: account.apiKey };
326
+ mkdirSync(dirname(globalAuthPath), { recursive: true, mode: 0o700 });
327
+ writeFileSync(globalAuthPath, `${JSON.stringify(current, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
328
+ try {
329
+ chmodSync(globalAuthPath, 0o600);
330
+ } catch {
331
+ // best-effort
332
+ }
333
+ } catch {
334
+ // best-effort sync
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Custom streamSimple implementation for tokenin provider that wraps OpenAI completions
340
+ * and automatically rotates to alternative saved keys on 401/429/budget exceeded errors.
341
+ */
342
+ export function createTokenInStreamSimple(options?: {
343
+ authPath?: string;
344
+ getAuthStorage?: () => AuthStorage | undefined;
345
+ onRotate?: (failedAccount: TokenInAccount, nextAccount: TokenInAccount, reason: string) => void;
346
+ streamSimple?: (model: Model<any>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
347
+ }) {
348
+ const authPath = options?.authPath ?? getTokenInAuthPath();
349
+ const baseApi = options?.streamSimple ? undefined : getApiProvider("openai-completions");
350
+ const streamSimple = options?.streamSimple ?? baseApi!.streamSimple.bind(baseApi!);
351
+
352
+ const activate = (account: TokenInAccount): void => {
353
+ const authStorage = options?.getAuthStorage?.();
354
+ if (authStorage) {
355
+ applyTokenInAccountToAuth(account, authStorage, authPath);
356
+ } else {
357
+ persistActiveTokenInAccount(account, authPath);
358
+ }
359
+ };
360
+
361
+ const withAccountAuthorization = (streamOptions: SimpleStreamOptions | undefined, account: TokenInAccount): SimpleStreamOptions => {
362
+ const headers = Object.fromEntries(
363
+ Object.entries(streamOptions?.headers ?? {}).filter(([name]) => name.toLowerCase() !== "authorization"),
364
+ );
365
+ return {
366
+ ...streamOptions,
367
+ apiKey: account.apiKey,
368
+ headers: { ...headers, Authorization: `Bearer ${account.apiKey}` },
369
+ };
370
+ };
371
+
372
+ return function tokenInStreamSimple(
373
+ model: Model<any>,
374
+ context: Context,
375
+ streamOptions?: SimpleStreamOptions,
376
+ ): AssistantMessageEventStream {
377
+ return lazyStream(model, async () => {
378
+ const auth = readTokenInAuth(authPath);
379
+ const accounts = auth.accounts;
380
+
381
+ // If no accounts saved, fall back directly to base implementation
382
+ if (accounts.length === 0) {
383
+ return streamSimple(model, context, streamOptions);
384
+ }
385
+
386
+ // Order accounts: active account first, followed by remaining accounts
387
+ const activeIndex = accounts.findIndex((a) => a.id === auth.activeId || a.apiKey === streamOptions?.apiKey);
388
+ const orderedAccounts: TokenInAccount[] = [];
389
+ if (activeIndex >= 0) {
390
+ orderedAccounts.push(accounts[activeIndex]!);
391
+ for (let i = 0; i < accounts.length; i++) {
392
+ if (i !== activeIndex) orderedAccounts.push(accounts[i]!);
393
+ }
394
+ } else {
395
+ orderedAccounts.push(...accounts);
396
+ }
397
+
398
+ // Filter/order candidates: prioritize those not currently on cooldown
399
+ const availableAccounts = orderedAccounts.filter((a) => !isTokenInOnCooldown(a.id));
400
+ const candidateAccounts = (availableAccounts.length > 0 ? availableAccounts : orderedAccounts).map((account) => ({ ...account }));
401
+
402
+ let lastErrorEvent: AssistantMessageEvent | undefined;
403
+ let lastThrownError: unknown;
404
+
405
+ for (let i = 0; i < candidateAccounts.length; i++) {
406
+ const currentAccount = candidateAccounts[i]!;
407
+
408
+ const requestOptions = withAccountAuthorization(streamOptions, currentAccount);
409
+
410
+ let underlyingStream: AssistantMessageEventStream;
411
+ try {
412
+ underlyingStream = streamSimple(model, context, requestOptions);
413
+ } catch (err) {
414
+ lastThrownError = err;
415
+ if (isRotateableTokenInError(err) && i + 1 < candidateAccounts.length) {
416
+ setTokenInCooldown(currentAccount.id);
417
+ const nextAccount = candidateAccounts[i + 1]!;
418
+ activate(nextAccount);
419
+ options?.onRotate?.(currentAccount, nextAccount, String(err));
420
+ continue;
421
+ }
422
+ throw err;
423
+ }
424
+
425
+ // Hold a leading start event until the request proves it produced output. This
426
+ // permits an immediate upstream error to be retried without exposing two starts.
427
+ const iterator = underlyingStream[Symbol.asyncIterator]();
428
+ let firstResult: IteratorResult<AssistantMessageEvent>;
429
+ try {
430
+ firstResult = await iterator.next();
431
+ } catch (err) {
432
+ lastThrownError = err;
433
+ if (isRotateableTokenInError(err) && i + 1 < candidateAccounts.length) {
434
+ setTokenInCooldown(currentAccount.id);
435
+ const nextAccount = candidateAccounts[i + 1]!;
436
+ activate(nextAccount);
437
+ options?.onRotate?.(currentAccount, nextAccount, String(err));
438
+ continue;
439
+ }
440
+ throw err;
441
+ }
442
+
443
+ if (firstResult.done) {
444
+ const passthrough = createAssistantMessageEventStream();
445
+ passthrough.end();
446
+ return passthrough;
447
+ }
448
+
449
+ const bufferedEvents = [firstResult.value];
450
+ if (bufferedEvents[0].type === "start") {
451
+ try {
452
+ const next = await iterator.next();
453
+ if (!next.done) bufferedEvents.push(next.value);
454
+ } catch (err) {
455
+ lastThrownError = err;
456
+ if (isRotateableTokenInError(err) && i + 1 < candidateAccounts.length) {
457
+ setTokenInCooldown(currentAccount.id);
458
+ const nextAccount = candidateAccounts[i + 1]!;
459
+ activate(nextAccount);
460
+ options?.onRotate?.(currentAccount, nextAccount, String(err));
461
+ continue;
462
+ }
463
+ throw err;
464
+ }
465
+ }
466
+
467
+ const immediateError = bufferedEvents.find((event) => event.type === "error");
468
+ if (immediateError?.type === "error" && isRotateableTokenInError(immediateError.error)) {
469
+ lastErrorEvent = immediateError;
470
+ setTokenInCooldown(currentAccount.id);
471
+ if (i + 1 < candidateAccounts.length) {
472
+ const nextAccount = candidateAccounts[i + 1]!;
473
+ activate(nextAccount);
474
+ options?.onRotate?.(currentAccount, nextAccount, immediateError.error.errorMessage ?? "Error");
475
+ continue;
476
+ }
477
+ }
478
+
479
+ const outputStream = createAssistantMessageEventStream();
480
+ (async () => {
481
+ try {
482
+ for (const event of bufferedEvents) outputStream.push(event);
483
+ while (true) {
484
+ const next = await iterator.next();
485
+ if (next.done) break;
486
+ outputStream.push(next.value);
487
+ }
488
+ outputStream.end();
489
+ } catch {
490
+ outputStream.end();
491
+ }
492
+ })();
493
+
494
+ return outputStream;
495
+ }
496
+
497
+ // If loop exhausted with a rotateable error event, return a stream with that error
498
+ if (lastErrorEvent) {
499
+ const errorStream = createAssistantMessageEventStream();
500
+ errorStream.push(lastErrorEvent);
501
+ errorStream.end();
502
+ return errorStream;
503
+ }
504
+
505
+ if (lastThrownError) {
506
+ throw lastThrownError;
507
+ }
508
+
509
+ return streamSimple(model, context, streamOptions);
510
+ });
511
+ };
512
+ }
513
+
219
514
  async function openDashboard(pi: ExtensionAPI): Promise<void> {
220
515
  const url = TOKEN_IN_DASHBOARD_URL;
221
516
  let command: string;
@@ -537,7 +832,14 @@ async function runOnboarding(pi: ExtensionAPI, ctx: ExtensionContext): Promise<v
537
832
  }
538
833
 
539
834
  export default function tokenInOnboardingExtension(pi: ExtensionAPI): void {
835
+ let authStorage: AuthStorage | undefined;
836
+ pi.registerProvider("tokenin", {
837
+ api: "openai-completions",
838
+ streamSimple: createTokenInStreamSimple({ getAuthStorage: () => authStorage }),
839
+ });
840
+
540
841
  pi.on("session_start", async (event: SessionStartEvent, ctx: ExtensionContext) => {
842
+ authStorage = ctx.modelRegistry.authStorage;
541
843
  if (event.reason !== "startup") return;
542
844
  if (isOnboardingComplete()) return;
543
845
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
5
5
  "type": "module",
6
6
  "repository": {