@gajae-code/coding-agent 0.12.11 → 0.12.13

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 (140) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/types/cli/marketplace-hint.d.ts +28 -0
  3. package/dist/types/cli/notify-cli.d.ts +42 -1
  4. package/dist/types/cli/update-cli.d.ts +9 -3
  5. package/dist/types/cli.d.ts +10 -0
  6. package/dist/types/commands/notify.d.ts +6 -0
  7. package/dist/types/config/file-lock-gc.d.ts +13 -1
  8. package/dist/types/config/file-lock.d.ts +3 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/models-config-schema.d.ts +99 -0
  11. package/dist/types/coordinator/contract.d.ts +1 -1
  12. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  13. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  14. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  15. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  16. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  17. package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
  18. package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
  19. package/dist/types/main.d.ts +6 -2
  20. package/dist/types/modes/acp/acp-agent.d.ts +24 -1
  21. package/dist/types/modes/components/session-selector.d.ts +1 -1
  22. package/dist/types/modes/utils/host-status.d.ts +23 -0
  23. package/dist/types/runtime-mcp/types.d.ts +2 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  25. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  26. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  28. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  29. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  30. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  31. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  32. package/dist/types/sdk/bus/html-format.d.ts +10 -0
  33. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  34. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  35. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  36. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  37. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  38. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
  42. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  43. package/dist/types/sdk/host/host.d.ts +52 -0
  44. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  45. package/dist/types/sdk/session-activation.d.ts +41 -0
  46. package/dist/types/sdk/session.d.ts +18 -0
  47. package/dist/types/sdk/startup-capability.d.ts +8 -1
  48. package/dist/types/session/agent-session.d.ts +7 -2
  49. package/dist/types/session/artifacts.d.ts +2 -2
  50. package/dist/types/session/session-manager.d.ts +40 -6
  51. package/dist/types/tools/index.d.ts +15 -0
  52. package/dist/types/utils/npm-registry.d.ts +71 -0
  53. package/examples/extensions/README.md +38 -4
  54. package/examples/extensions/ooo-bridge.ts +15 -0
  55. package/package.json +8 -8
  56. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  57. package/src/async/job-manager.ts +9 -2
  58. package/src/cli/fast-help.ts +1 -0
  59. package/src/cli/marketplace-hint.ts +39 -0
  60. package/src/cli/notify-cli.ts +226 -2
  61. package/src/cli/plugin-cli.ts +10 -0
  62. package/src/cli/update-cli.ts +90 -23
  63. package/src/cli.ts +23 -0
  64. package/src/commands/notify.ts +27 -3
  65. package/src/commands/sdk.ts +1 -0
  66. package/src/config/file-lock-gc.ts +65 -26
  67. package/src/config/file-lock.ts +61 -16
  68. package/src/config/model-profiles.ts +46 -30
  69. package/src/config/model-registry.ts +17 -5
  70. package/src/config/models-config-schema.ts +9 -4
  71. package/src/coordinator/contract.ts +1 -0
  72. package/src/coordinator-mcp/question-state.ts +2 -0
  73. package/src/coordinator-mcp/server.ts +358 -7
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  76. package/src/extensibility/extensions/index.ts +1 -0
  77. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  78. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  79. package/src/extensibility/extensions/runner.ts +15 -2
  80. package/src/extensibility/extensions/types.ts +2 -0
  81. package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
  82. package/src/gjc-runtime/deep-interview-state.ts +27 -3
  83. package/src/gjc-runtime/gc-render.ts +8 -1
  84. package/src/gjc-runtime/gc-runtime.ts +15 -1
  85. package/src/gjc-runtime/launch-tmux.ts +53 -1
  86. package/src/internal-urls/docs-index.generated.ts +14 -13
  87. package/src/main.ts +214 -70
  88. package/src/modes/DESIGN.md +8 -5
  89. package/src/modes/acp/acp-agent.ts +123 -7
  90. package/src/modes/components/footer.ts +10 -18
  91. package/src/modes/components/model-selector.ts +52 -5
  92. package/src/modes/components/session-selector.ts +26 -3
  93. package/src/modes/components/status-line/gh.ts +23 -2
  94. package/src/modes/controllers/event-controller.ts +3 -0
  95. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  96. package/src/modes/controllers/input-controller.ts +11 -0
  97. package/src/modes/controllers/selector-controller.ts +8 -2
  98. package/src/modes/interactive-mode.ts +7 -1
  99. package/src/modes/print-mode.ts +10 -0
  100. package/src/modes/utils/host-status.ts +38 -0
  101. package/src/prompts/system/project-prompt.md +19 -0
  102. package/src/runtime-mcp/types.ts +2 -0
  103. package/src/sdk/broker/lifecycle.ts +60 -3
  104. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  105. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  106. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  107. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  108. package/src/sdk/bus/control-plane-frames.ts +51 -0
  109. package/src/sdk/bus/conversation-store.ts +29 -1
  110. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  111. package/src/sdk/bus/html-format.ts +19 -3
  112. package/src/sdk/bus/index.ts +91 -12
  113. package/src/sdk/bus/notification-service.ts +28 -1
  114. package/src/sdk/bus/rich-render.ts +3 -2
  115. package/src/sdk/bus/slack-daemon.ts +99 -4
  116. package/src/sdk/bus/slack-live-provider.ts +32 -2
  117. package/src/sdk/bus/slack-provider.ts +11 -0
  118. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  119. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  120. package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
  121. package/src/sdk/bus/telegram-daemon.ts +2007 -509
  122. package/src/sdk/bus/telegram-reference.ts +114 -15
  123. package/src/sdk/bus/topic-registry.ts +674 -181
  124. package/src/sdk/host/host.ts +180 -1
  125. package/src/sdk/lifecycle-session.ts +8 -2
  126. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  127. package/src/sdk/session-activation.ts +127 -0
  128. package/src/sdk/session.ts +144 -29
  129. package/src/sdk/startup-capability.ts +10 -1
  130. package/src/session/agent-session.ts +220 -13
  131. package/src/session/artifacts.ts +56 -13
  132. package/src/session/internal/managed-session-storage.ts +228 -7
  133. package/src/session/session-manager.ts +225 -60
  134. package/src/setup/provider-presets.json +3 -3
  135. package/src/task/index.ts +92 -44
  136. package/src/thinking-metadata.ts +2 -1
  137. package/src/tools/ask.ts +10 -0
  138. package/src/tools/index.ts +15 -0
  139. package/src/tools/todo-write.ts +4 -4
  140. package/src/utils/npm-registry.ts +660 -0
package/src/main.ts CHANGED
@@ -61,11 +61,13 @@ import { SessionMigrationBusyError } from "./session/internal/session-open-error
61
61
  import {
62
62
  type ResumeSessionIdentity,
63
63
  resolveResumableSession,
64
+ SESSION_OVERSIZED_RECOVERY_MESSAGE,
64
65
  SessionArtifactCapacityError,
65
66
  type SessionDestination,
66
67
  type SessionDirectoryMigrationPolicy,
67
68
  type SessionInfo,
68
69
  SessionManager,
70
+ SessionTranscriptOversizedError,
69
71
  type StrictSessionOpenResult,
70
72
  } from "./session/session-manager";
71
73
  import { runStartupCredentialAutoImportIfNeeded } from "./setup/credential-auto-import";
@@ -76,20 +78,13 @@ import { persistTaskTokenLog, resolveTaskTokenLogDir, taskTokenLogFromUsage } fr
76
78
  import type { LspStartupServerInfo } from "./tools";
77
79
  import { getDisplayChangelogEntries, getInstalledVersionChangelogEntry, getNewEntries } from "./utils/changelog";
78
80
  import type { EventBus } from "./utils/event-bus";
81
+ import { fetchLatestPackageVersion } from "./utils/npm-registry";
79
82
 
80
83
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
81
84
  try {
82
- const response = await fetch("https://registry.npmjs.org/@gajae-code/coding-agent/latest");
83
- if (!response.ok) return undefined;
84
-
85
- const data = (await response.json()) as { version?: string };
86
- const latestVersion = data.version;
87
-
88
- if (latestVersion && Bun.semver.order(latestVersion, currentVersion) > 0) {
89
- return latestVersion;
90
- }
91
-
92
- return undefined;
85
+ // Resolved from npm config so mirrored/firewalled networks are checked too.
86
+ const { version } = await fetchLatestPackageVersion("@gajae-code/coding-agent");
87
+ return Bun.semver.order(version, currentVersion) > 0 ? version : undefined;
93
88
  } catch {
94
89
  return undefined;
95
90
  }
@@ -406,6 +401,7 @@ type StartupModelProfileArgs = {
406
401
  parsedArgs: Pick<Args, "default" | "model" | "mpreset" | "thinking">;
407
402
  startupModel?: CreateAgentSessionOptions["model"];
408
403
  startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
404
+ preferCachedModels?: boolean;
409
405
  };
410
406
 
411
407
  async function applyStartupModelProfilesWithPolicy(
@@ -416,16 +412,17 @@ async function applyStartupModelProfilesWithPolicy(
416
412
  profileName: string,
417
413
  persistDefault: boolean,
418
414
  options: { thinkingLevelOverride?: CreateAgentSessionOptions["thinkingLevel"] } = {},
419
- ): Promise<void> => {
415
+ ): Promise<boolean> => {
420
416
  try {
421
417
  await activateModelProfile(
422
418
  { session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
423
419
  { persistDefault, thinkingLevelOverride: options.thinkingLevelOverride },
424
420
  );
421
+ return true;
425
422
  } catch (error) {
426
423
  if (onCredentialError && error instanceof ModelProfileCredentialError) {
427
424
  onCredentialError(error);
428
- return;
425
+ return false;
429
426
  }
430
427
  throw error;
431
428
  }
@@ -436,19 +433,40 @@ async function applyStartupModelProfilesWithPolicy(
436
433
  // deferred `--model <pattern>` path resolved inside createAgentSession.
437
434
  const explicitModel = args.parsedArgs.model ? (args.startupModel ?? args.session.model) : undefined;
438
435
  const defaultProfile = args.settings.get("modelProfile.default");
439
- if (defaultProfile || args.parsedArgs.mpreset) {
440
- await args.modelRegistry.refresh("online-if-uncached");
441
- }
436
+ const preferCachedProfiles = args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined;
437
+ const applyConfiguredProfiles = async (): Promise<boolean> => {
438
+ let applied = true;
439
+ if (defaultProfile) {
440
+ applied =
441
+ (await applyProfile(defaultProfile, false, {
442
+ thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
443
+ ? args.settings.get("defaultThinkingLevel")
444
+ : undefined,
445
+ })) && applied;
446
+ }
447
+ if (args.parsedArgs.mpreset) {
448
+ applied = (await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true)) && applied;
449
+ }
450
+ return applied;
451
+ };
442
452
 
443
- if (defaultProfile) {
444
- await applyProfile(defaultProfile, false, {
445
- thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
446
- ? args.settings.get("defaultThinkingLevel")
447
- : undefined,
448
- });
449
- }
450
- if (args.parsedArgs.mpreset) {
451
- await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true);
453
+ if (preferCachedProfiles) {
454
+ let applied: boolean;
455
+ let refreshedOnline = false;
456
+ try {
457
+ applied = await applyConfiguredProfiles();
458
+ } catch (error) {
459
+ if (error instanceof ModelProfileCredentialError) throw error;
460
+ await args.modelRegistry.refresh("online-if-uncached");
461
+ refreshedOnline = true;
462
+ applied = await applyConfiguredProfiles();
463
+ }
464
+ if (applied && !refreshedOnline) args.modelRegistry.refreshInBackground();
465
+ } else {
466
+ if (defaultProfile || args.parsedArgs.mpreset) {
467
+ await args.modelRegistry.refresh("online-if-uncached");
468
+ }
469
+ await applyConfiguredProfiles();
452
470
  }
453
471
 
454
472
  // Explicit CLI --model/--thinking must win over any activated or skipped profile.
@@ -509,20 +527,52 @@ export async function applyStartupModelProfilesForRoot(
509
527
  resumeAction: "continue-tail" | "open-idle" | undefined;
510
528
  },
511
529
  ): Promise<{ recoverableErrors: string[] }> {
530
+ const policyArgs = { ...args, preferCachedModels: args.isInteractive };
512
531
  if (!isStartupModelProfileCredentialRecoveryEligible(args)) {
513
- await applyStartupModelProfilesOrExit(args);
532
+ try {
533
+ await applyStartupModelProfilesWithPolicy(policyArgs);
534
+ } catch (error) {
535
+ await exitForStartupModelProfileError(args, error);
536
+ }
514
537
  return { recoverableErrors: [] };
515
538
  }
516
539
 
517
540
  const recoverableErrors: string[] = [];
518
541
  try {
519
- await applyStartupModelProfilesWithPolicy(args, error => recoverableErrors.push(error.message));
542
+ await applyStartupModelProfilesWithPolicy(policyArgs, error => recoverableErrors.push(error.message));
520
543
  } catch (error) {
521
544
  await exitForStartupModelProfileError(args, error);
522
545
  }
523
546
  return { recoverableErrors };
524
547
  }
525
548
 
549
+ type DeferredModelProfileStartup = () => Promise<{ recoverableErrors: string[] }>;
550
+ class DeferredModelProfileStartupError extends Error {
551
+ readonly profileError: unknown;
552
+
553
+ constructor(profileError: unknown) {
554
+ super("Deferred model profile startup failed");
555
+ this.profileError = profileError;
556
+ }
557
+ }
558
+
559
+ async function applyDeferredStartupModelProfilesForRoot(
560
+ args: StartupModelProfileArgs & {
561
+ isInteractive: boolean;
562
+ hasInteractiveTerminal: boolean;
563
+ initialMessage: string | undefined;
564
+ initialMessages: readonly string[];
565
+ resumeAction: "continue-tail" | "open-idle" | undefined;
566
+ },
567
+ ): Promise<{ recoverableErrors: string[] }> {
568
+ const recoverableErrors: string[] = [];
569
+ const onCredentialError = isStartupModelProfileCredentialRecoveryEligible(args)
570
+ ? (error: ModelProfileCredentialError) => recoverableErrors.push(error.message)
571
+ : undefined;
572
+ await applyStartupModelProfilesWithPolicy({ ...args, preferCachedModels: true }, onCredentialError);
573
+ return { recoverableErrors };
574
+ }
575
+
526
576
  interface InteractiveModeFactoryOptions {
527
577
  session: AgentSession;
528
578
  version: string;
@@ -572,10 +622,13 @@ function operatorFacingSessionOpenMessage(value: unknown): string | undefined {
572
622
  ? value.code
573
623
  : value instanceof SessionMigrationBusyError
574
624
  ? value.code
575
- : typeof value === "string"
576
- ? value
577
- : undefined;
625
+ : value instanceof SessionTranscriptOversizedError
626
+ ? value.code
627
+ : typeof value === "string"
628
+ ? value
629
+ : undefined;
578
630
  if (code === "artifact_capacity_exceeded") return SESSION_ARTIFACT_CAPACITY_RECOVERY_MESSAGE;
631
+ if (code === "oversized") return SESSION_OVERSIZED_RECOVERY_MESSAGE;
579
632
  if (code === "migration_busy") return new SessionMigrationBusyError().message;
580
633
  return undefined;
581
634
  }
@@ -616,6 +669,8 @@ export async function runInteractiveMode(
616
669
  initialImages?: ImageContent[],
617
670
  createInteractiveMode?: CreateInteractiveMode,
618
671
  resumeAction?: "continue-tail" | "open-idle",
672
+ startDeferredMcpConfig?: CreateAgentSessionResult["startDeferredMcpConfig"],
673
+ startDeferredModelProfiles?: DeferredModelProfileStartup,
619
674
  ): Promise<void> {
620
675
  const mode = createInteractiveMode
621
676
  ? createInteractiveMode({
@@ -659,45 +714,98 @@ export async function runInteractiveMode(
659
714
  mode.showStatus(notify.message);
660
715
  }
661
716
  }
717
+ if (startDeferredMcpConfig) {
718
+ mode.showStatus("Connecting MCP tools…");
719
+ void startDeferredMcpConfig()
720
+ .then(result => {
721
+ if (result.hasErrors) {
722
+ mode.showWarning(
723
+ result.loadedToolCount > 0 ? "Some MCP tools could not be loaded." : "MCP tools could not be loaded.",
724
+ );
725
+ return;
726
+ }
727
+ mode.showStatus(`${result.loadedToolCount} MCP tool${result.loadedToolCount === 1 ? "" : "s"} connected`);
728
+ })
729
+ .catch(() => {
730
+ logger.warn("Deferred MCP startup failed.");
731
+ mode.showError("MCP tools could not be loaded.");
732
+ });
733
+ }
734
+ let deferredModelProfileFailure: Promise<never> | undefined;
735
+ if (startDeferredModelProfiles) {
736
+ mode.showStatus("Loading model profile…");
737
+ deferredModelProfileFailure = startDeferredModelProfiles().then(
738
+ result => {
739
+ for (const error of result.recoverableErrors) mode.showError(error);
740
+ mode.showStatus("Model profile ready");
741
+ return Promise.withResolvers<never>().promise;
742
+ },
743
+ error => {
744
+ const message = error instanceof Error ? error.message : "Model profile could not be loaded.";
745
+ mode.showError(message);
746
+ throw new DeferredModelProfileStartupError(error);
747
+ },
748
+ );
749
+ }
662
750
 
663
- const hasStartupInput = initialMessage !== undefined || initialMessages.length > 0;
664
- if (!hasStartupInput && resumeAction === "continue-tail") {
665
- try {
666
- await session.continuePersistedHistory();
667
- } catch (error: unknown) {
668
- const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
669
- mode.showError(errorMessage);
751
+ const runStartupInputAndPromptLoop = async (): Promise<never> => {
752
+ const hasStartupInput = initialMessage !== undefined || initialMessages.length > 0;
753
+ if (!hasStartupInput && resumeAction === "continue-tail") {
754
+ try {
755
+ await session.continuePersistedHistory();
756
+ } catch (error: unknown) {
757
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
758
+ mode.showError(errorMessage);
759
+ }
670
760
  }
671
- }
672
761
 
673
- if (initialMessage !== undefined) {
674
- try {
675
- await session.prompt(initialMessage, { images: initialImages });
676
- } catch (error: unknown) {
677
- const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
678
- mode.showError(errorMessage);
762
+ if (initialMessage !== undefined) {
763
+ try {
764
+ await session.prompt(initialMessage, { images: initialImages });
765
+ } catch (error: unknown) {
766
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
767
+ mode.showError(errorMessage);
768
+ }
679
769
  }
680
- }
681
770
 
682
- for (const message of initialMessages) {
683
- try {
684
- let text = message;
685
- const slashResult = await executeBuiltinSlashCommand(text, {
686
- ctx: mode,
687
- handleBackgroundCommand: () => mode.handleBackgroundCommand(),
688
- });
689
- if (slashResult === true) continue;
690
- if (typeof slashResult === "string") text = slashResult;
691
- await session.prompt(text);
692
- } catch (error: unknown) {
693
- const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
694
- mode.showError(errorMessage);
771
+ for (const message of initialMessages) {
772
+ try {
773
+ let text = message;
774
+ const slashResult = await executeBuiltinSlashCommand(text, {
775
+ ctx: mode,
776
+ handleBackgroundCommand: () => mode.handleBackgroundCommand(),
777
+ });
778
+ if (slashResult === true) continue;
779
+ if (typeof slashResult === "string") text = slashResult;
780
+ await session.prompt(text);
781
+ } catch (error: unknown) {
782
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
783
+ mode.showError(errorMessage);
784
+ }
695
785
  }
696
- }
697
786
 
698
- while (true) {
699
- const input = await mode.getUserInput();
700
- await submitInteractiveInput(mode, session, input);
787
+ while (true) {
788
+ const input = await mode.getUserInput();
789
+ await submitInteractiveInput(mode, session, input);
790
+ }
791
+ };
792
+
793
+ const inputLoop = runStartupInputAndPromptLoop();
794
+ if (!deferredModelProfileFailure) {
795
+ await inputLoop;
796
+ return;
797
+ }
798
+ try {
799
+ await Promise.race([inputLoop, deferredModelProfileFailure]);
800
+ } catch (error) {
801
+ if (!(error instanceof DeferredModelProfileStartupError)) throw error;
802
+ try {
803
+ await mode.shutdown();
804
+ } finally {
805
+ mode.stop();
806
+ }
807
+ await inputLoop.catch(() => {});
808
+ throw error.profileError;
701
809
  }
702
810
  }
703
811
 
@@ -1494,7 +1602,13 @@ export async function runRootCommand(
1494
1602
  sessionOptions.notificationHostModeSupported = isInteractive;
1495
1603
  sessionOptions.sdkHostModeSupported = isInteractive;
1496
1604
  sessionOptions.settings = settingsInstance;
1605
+ if (isInteractive && sessionOptions.mcpConfigPath) {
1606
+ sessionOptions.deferMcpConfigStartup = true;
1607
+ }
1497
1608
  const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
1609
+ const deferMemoryBackendStartup =
1610
+ hasRootStartupProfile && !(parsedArgs.authBootstrap === true && isInteractive) && mode !== "acp";
1611
+ sessionOptions.deferMemoryBackendStartup = deferMemoryBackendStartup;
1498
1612
 
1499
1613
  // Research-mode (RLM) preset: augment session options before session creation.
1500
1614
  deps.rlmPreset?.applyOptions(sessionOptions, settingsInstance);
@@ -1574,10 +1688,16 @@ export async function runRootCommand(
1574
1688
  if (!deps.suppressProcessExit) await postmortem.cleanup();
1575
1689
  }
1576
1690
  } else {
1577
- const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } = await createSession(
1578
- sessionOptions,
1579
- { skipPostCreateModelRefresh: hasRootStartupProfile },
1580
- );
1691
+ const {
1692
+ session,
1693
+ setToolUIContext,
1694
+ modelFallbackMessage,
1695
+ lspServers,
1696
+ mcpManager,
1697
+ startDeferredMcpConfig,
1698
+ startDeferredMemoryBackend,
1699
+ eventBus,
1700
+ } = await createSession(sessionOptions, { skipPostCreateModelRefresh: hasRootStartupProfile });
1581
1701
  applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, session.model);
1582
1702
 
1583
1703
  // Research-mode (RLM) preset: hard tool-boundary assertion after the registry is assembled.
@@ -1594,8 +1714,9 @@ export async function runRootCommand(
1594
1714
  }
1595
1715
  }
1596
1716
 
1717
+ let startDeferredModelProfiles: DeferredModelProfileStartup | undefined;
1597
1718
  if (!(parsedArgs.authBootstrap === true && isInteractive)) {
1598
- const { recoverableErrors } = await applyStartupModelProfilesForRoot({
1719
+ const profileArgs = {
1599
1720
  session,
1600
1721
  settings: settingsInstance,
1601
1722
  modelRegistry,
@@ -1607,9 +1728,27 @@ export async function runRootCommand(
1607
1728
  initialMessage,
1608
1729
  initialMessages: parsedArgs.messages,
1609
1730
  resumeAction: bareResumeAction,
1610
- });
1611
- for (const recoverableError of recoverableErrors) {
1612
- notifs.push({ kind: "error", message: recoverableError });
1731
+ };
1732
+ if (isInteractive && parsedArgs.mpreset) {
1733
+ const ready = Promise.withResolvers<void>();
1734
+ session.extendStartupTurnBarrier(ready.promise);
1735
+ startDeferredModelProfiles = async () => {
1736
+ try {
1737
+ const result = await applyDeferredStartupModelProfilesForRoot(profileArgs);
1738
+ startDeferredMemoryBackend?.();
1739
+ ready.resolve();
1740
+ return result;
1741
+ } catch (error) {
1742
+ ready.reject(error);
1743
+ throw error;
1744
+ }
1745
+ };
1746
+ } else {
1747
+ const { recoverableErrors } = await applyStartupModelProfilesForRoot(profileArgs);
1748
+ startDeferredMemoryBackend?.();
1749
+ for (const recoverableError of recoverableErrors) {
1750
+ notifs.push({ kind: "error", message: recoverableError });
1751
+ }
1613
1752
  }
1614
1753
  }
1615
1754
 
@@ -1690,6 +1829,8 @@ export async function runRootCommand(
1690
1829
  initialImages,
1691
1830
  deps.createInteractiveMode,
1692
1831
  bareResumeAction,
1832
+ startDeferredMcpConfig,
1833
+ startDeferredModelProfiles,
1693
1834
  );
1694
1835
  }
1695
1836
  } catch (error) {
@@ -1698,6 +1839,9 @@ export async function runRootCommand(
1698
1839
  } catch {
1699
1840
  logger.warn("Failed to dispose session after interactive error");
1700
1841
  }
1842
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "cancelled") {
1843
+ return;
1844
+ }
1701
1845
  throw error;
1702
1846
  }
1703
1847
 
@@ -63,9 +63,11 @@ visible-width-aligned column capped at 30 cells, two spaces, then a truncated
63
63
  value. The selected row uses the themed cursor; unselected rows reserve two
64
64
  spaces. It centers the selected item inside its `maxVisible` window, reports
65
65
  scroll position as `(current/total)`, places a blank row before the selected
66
- item description, and ends with the dim
67
- `Enter/Space to change · Esc to cancel` hint. Hosts that need stable height
68
- reserve fixed description rows; the status-line custom editor reserves two.
66
+ item description, and ends with a dim keyboard hint. Printable text starts a
67
+ case-insensitive label search shown above the list; Backspace removes one
68
+ grapheme, and Escape clears a non-empty search before a later Escape cancels
69
+ the list. Hosts that need stable height reserve fixed description rows; the
70
+ status-line custom editor reserves two.
69
71
 
70
72
  Submenus are a content replacement, not a modal overlay: a bold accent title,
71
73
  optional muted description, optional preview, a spacer, a select/list control,
@@ -82,8 +84,9 @@ empty space and list density for an operational setup flow.
82
84
  ### Focus, cursor, keyboard, and input behavior
83
85
 
84
86
  - Up/Down wrap within selector lists. Enter and Space activate the current
85
- action. Escape follows the current component's cancel path before the parent
86
- is allowed to close.
87
+ action. Printable text filters settings by label. Escape first clears an
88
+ active filter, then follows the current component's cancel path before the
89
+ parent is allowed to close.
87
90
  - The parent routes Tab/Left/Right to the tab bar except while a text input is
88
91
  active. Text entry owns arrow keys and Tab in that state.
89
92
  - `Input` has a visible `> ` prompt, a zero-width hardware cursor marker only
@@ -403,8 +403,8 @@ function receivedSdkEvent(frame: JsonObject): ReceivedSdkEvent | undefined {
403
403
  }
404
404
 
405
405
  const ACP_CONFIG_OPTIONS = [
406
- { id: MODEL_CONFIG_ID, name: "Model", options: [] },
407
- { id: THINKING_CONFIG_ID, name: "Thinking", options: [] },
406
+ { id: MODEL_CONFIG_ID, name: "Model", category: "model", options: [] },
407
+ { id: THINKING_CONFIG_ID, name: "Thinking", category: "thought_level", options: [] },
408
408
  {
409
409
  id: "steeringMode",
410
410
  name: "Steering queue",
@@ -466,17 +466,122 @@ function modelPresetConfigOptions(query: unknown, current: string): { value: str
466
466
  return [...options].map(([value, name]) => ({ value, name }));
467
467
  }
468
468
 
469
- function modelConfigOptions(query: unknown, current: string | undefined): { value: string; name: string }[] {
469
+ function modelConfigOptions(
470
+ query: unknown,
471
+ current: string | undefined,
472
+ activeProviders?: ReadonlySet<string>,
473
+ ): { value: string; name: string }[] {
470
474
  const options = new Map<string, string>();
471
475
  for (const item of pageItems(query)) {
472
476
  const model = object(item);
473
477
  if (!model || typeof model.provider !== "string" || typeof model.id !== "string") continue;
478
+ if (activeProviders !== undefined && !activeProviders.has(model.provider)) continue;
474
479
  const value = `${model.provider}/${model.id}`;
475
480
  options.set(value, typeof model.name === "string" ? model.name : value);
476
481
  }
477
482
  if (current && !options.has(current)) options.set(current, current);
478
483
  return [...options].map(([value, name]) => ({ value, name }));
479
484
  }
485
+ const MAX_ACTIVE_PROVIDER_PAGES = 100;
486
+
487
+ /**
488
+ * Unsupported-query compatibility fallback. Session hosts without
489
+ * `providers.list/active` reject the unknown named query as either
490
+ * `operation_not_session_owned` (host knows the registry but not the query)
491
+ * or `invalid_request` (pre-Q29 host that predates the registry entry). Both
492
+ * keep the full catalog authoritative on the first page; every other failure
493
+ * mode fails closed so the active-provider contract is never silently
494
+ * widened.
495
+ */
496
+ function isUnsupportedQueryError(error: unknown): boolean {
497
+ return (
498
+ typeof error === "object" &&
499
+ error !== null &&
500
+ ((error as { code?: unknown }).code === "operation_not_session_owned" ||
501
+ (error as { code?: unknown }).code === "invalid_request")
502
+ );
503
+ }
504
+
505
+ function queryPage(query: unknown): { items?: unknown; complete?: unknown; continuationCursor?: unknown } | undefined {
506
+ const response = object(query);
507
+ const result = object(response?.result) ?? response;
508
+ return object(result?.page);
509
+ }
510
+
511
+ /**
512
+ * Collect every page of `providers.list/active` (Q29, GJC >= 0.12.8) and
513
+ * return the providers with usable stored credentials or credentialless
514
+ * connection kinds, mirroring the TUI model picker's
515
+ * `modelRegistry.getAvailable()`. The openwebui-gjc-adapter applies the same
516
+ * filter to `/v1/models`. Q29 pages are byte-bounded and can span multiple
517
+ * pages when custom provider ids inflate the payload, so all pages are
518
+ * consumed before the provider set is built. Returns undefined only when the
519
+ * session host rejects the query with `operation_not_session_owned`; any
520
+ * other query failure or malformed page is thrown.
521
+ */
522
+ export async function collectActiveProviderIds(
523
+ adapter: Pick<AcpSdkAdapter, "query">,
524
+ ): Promise<ReadonlySet<string> | undefined> {
525
+ const providers = new Set<string>();
526
+ let cursor: string | undefined;
527
+ for (let pageCount = 0; pageCount < MAX_ACTIVE_PROVIDER_PAGES; pageCount++) {
528
+ let response: unknown;
529
+ try {
530
+ response = await adapter.query("providers.list/active", {}, cursor);
531
+ } catch (error) {
532
+ if (cursor === undefined && isUnsupportedQueryError(error)) return undefined;
533
+ throw error;
534
+ }
535
+ const page = queryPage(response);
536
+ if (!page) throw new AcpSdkAdapterError("protocol_error", "providers.list/active returned no page.");
537
+ const items = page.items;
538
+ if (!Array.isArray(items))
539
+ throw new AcpSdkAdapterError("protocol_error", "providers.list/active returned a malformed page.");
540
+ for (const item of items) {
541
+ const record = object(item);
542
+ if (!record || typeof record.provider !== "string") continue;
543
+ const connectionKind = record.connectionKind;
544
+ if (connectionKind === "credential" || connectionKind === "credentialless") providers.add(record.provider);
545
+ }
546
+ if (page.complete === true) return providers;
547
+ if (typeof page.continuationCursor !== "string")
548
+ throw new AcpSdkAdapterError(
549
+ "protocol_error",
550
+ "providers.list/active page is incomplete without a continuation cursor.",
551
+ );
552
+ cursor = page.continuationCursor;
553
+ }
554
+ throw new AcpSdkAdapterError("protocol_error", "providers.list/active exceeded the page budget.");
555
+ }
556
+ const MAX_MODEL_CATALOG_PAGES = 100;
557
+
558
+ /**
559
+ * Collect every page of `models.list/current` (Q10) into one catalog so the
560
+ * active-provider filter never drops models that only appear on later pages.
561
+ * The SDK pages Q10 at a fixed byte target (256 KiB), which a fully
562
+ * configured catalog can exceed. Returns the same
563
+ * `{ result: { page: { items } } }` envelope `pageItems` consumes.
564
+ */
565
+ export async function collectModelCatalog(adapter: Pick<AcpSdkAdapter, "query">): Promise<unknown> {
566
+ const items: unknown[] = [];
567
+ let cursor: string | undefined;
568
+ for (let pageCount = 0; pageCount < MAX_MODEL_CATALOG_PAGES; pageCount++) {
569
+ const response = await adapter.query("models.list/current", {}, cursor);
570
+ const page = queryPage(response);
571
+ if (!page) throw new AcpSdkAdapterError("protocol_error", "models.list/current returned no page.");
572
+ if (!Array.isArray(page.items))
573
+ throw new AcpSdkAdapterError("protocol_error", "models.list/current returned a malformed page.");
574
+ items.push(...page.items);
575
+ if (page.complete === true) return { result: { page: { items } } };
576
+ if (typeof page.continuationCursor !== "string")
577
+ throw new AcpSdkAdapterError(
578
+ "protocol_error",
579
+ "models.list/current page is incomplete without a continuation cursor.",
580
+ );
581
+ cursor = page.continuationCursor;
582
+ }
583
+ throw new AcpSdkAdapterError("protocol_error", "models.list/current exceeded the page budget.");
584
+ }
480
585
 
481
586
  const THINKING_CONFIG_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"].map(value => ({
482
587
  value,
@@ -484,7 +589,12 @@ const THINKING_CONFIG_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhi
484
589
  }));
485
590
 
486
591
  /** Maps live canonical SDK config and the selected model catalog into the ACP 1.2.1 session state surface. */
487
- export function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: unknown, modelPreset?: string) {
592
+ export function acpSessionStateFromConfig(
593
+ query: unknown,
594
+ modelCatalogQuery?: unknown,
595
+ modelPreset?: string,
596
+ activeProviders?: ReadonlySet<string>,
597
+ ) {
488
598
  const values = configValues(query);
489
599
  const useModelPresets = modelPreset !== undefined;
490
600
  const currentModeId = values.get(MODE_CONFIG_ID) === ACP_PLAN_MODE_ID ? ACP_PLAN_MODE_ID : ACP_DEFAULT_MODE_ID;
@@ -493,6 +603,7 @@ export function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: un
493
603
  {
494
604
  id: MODE_CONFIG_ID,
495
605
  name: "Mode",
606
+ category: "mode" as const,
496
607
  type: "select" as const,
497
608
  currentValue: currentModeId,
498
609
  options: [
@@ -510,7 +621,7 @@ export function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: un
510
621
  option.id === MODEL_CONFIG_ID
511
622
  ? useModelPresets
512
623
  ? modelPresetConfigOptions(modelCatalogQuery, value)
513
- : modelConfigOptions(modelCatalogQuery, value)
624
+ : modelConfigOptions(modelCatalogQuery, value, activeProviders)
514
625
  : option.id === THINKING_CONFIG_ID
515
626
  ? THINKING_CONFIG_OPTIONS
516
627
  : [...option.options];
@@ -1799,8 +1910,13 @@ export class AcpAgent implements Agent {
1799
1910
  const modelPreset = this.#startupOptions?.modelPreset;
1800
1911
  const [config, modelCatalog] = await Promise.all([
1801
1912
  record.adapter.query("config.list/get"),
1802
- record.adapter.query(modelPreset === undefined ? "models.list/current" : "models.profiles.list"),
1913
+ modelPreset === undefined ? collectModelCatalog(record.adapter) : record.adapter.query("models.profiles.list"),
1803
1914
  ]);
1915
+ // Resolve usable providers in parallel. Only an older session host that
1916
+ // rejects `providers.list/active` with `operation_not_session_owned`
1917
+ // falls back to the full catalog; operational failures fail closed so
1918
+ // the active-provider contract is not silently widened.
1919
+ const activeProviders = modelPreset === undefined ? await collectActiveProviderIds(record.adapter) : undefined;
1804
1920
  record.authFailure = undefined;
1805
1921
  if (modelPreset !== undefined) {
1806
1922
  const activePreset = configValues(config).get(MODEL_PRESET_CONFIG_KEY);
@@ -1815,7 +1931,7 @@ export class AcpAgent implements Agent {
1815
1931
  throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
1816
1932
  }
1817
1933
  }
1818
- return acpSessionStateFromConfig(config, modelCatalog, modelPreset);
1934
+ return acpSessionStateFromConfig(config, modelCatalog, modelPreset, activeProviders);
1819
1935
  }
1820
1936
 
1821
1937
  async #publishAvailableCommands(id: string, adapter: AcpSdkAdapter): Promise<void> {
@@ -116,24 +116,16 @@ export class FooterComponent implements Component {
116
116
  render(width: number): string[] {
117
117
  const state = this.session.state;
118
118
 
119
- // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
120
- let totalInput = 0;
121
- let totalOutput = 0;
122
- let totalCacheRead = 0;
123
- let totalCacheWrite = 0;
124
- let totalCost = 0;
125
- let totalPremiumRequests = 0;
126
-
127
- for (const entry of this.session.sessionManager.getEntries()) {
128
- if (entry.type === "message" && entry.message.role === "assistant") {
129
- totalInput += entry.message.usage.input;
130
- totalOutput += entry.message.usage.output;
131
- totalCacheRead += entry.message.usage.cacheRead;
132
- totalCacheWrite += entry.message.usage.cacheWrite;
133
- totalCost += entry.message.usage.cost.total;
134
- totalPremiumRequests += entry.message.usage.premiumRequests ?? 0;
135
- }
136
- }
119
+ // Use the session manager's cumulative index so parent and task usage are
120
+ // accounted for consistently without rescanning persisted entries here.
121
+ const {
122
+ input: totalInput,
123
+ output: totalOutput,
124
+ cacheRead: totalCacheRead,
125
+ cacheWrite: totalCacheWrite,
126
+ cost: totalCost,
127
+ premiumRequests: totalPremiumRequests,
128
+ } = this.session.sessionManager.getUsageStatistics();
137
129
 
138
130
  // Calculate context usage from session (handles compaction correctly).
139
131
  // After compaction, tokens are unknown until the next LLM response.