@gajae-code/coding-agent 0.15.6 → 0.16.0

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 (167) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/root-flags.d.ts +14 -0
  4. package/dist/types/cli/update-cli.d.ts +13 -0
  5. package/dist/types/cli.d.ts +7 -0
  6. package/dist/types/commands/launch.d.ts +10 -0
  7. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  8. package/dist/types/config/file-lock.d.ts +5 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  13. package/dist/types/crash/record-loader.d.ts +15 -0
  14. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  15. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  17. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  18. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  19. package/dist/types/master-mode/context.d.ts +21 -0
  20. package/dist/types/master-mode/first-request.d.ts +34 -0
  21. package/dist/types/master-mode/snapshot.d.ts +32 -0
  22. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  24. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  25. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  26. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  27. package/dist/types/modes/print-mode.d.ts +17 -0
  28. package/dist/types/modes/ui-language.d.ts +7 -1
  29. package/dist/types/sdk/broker/broker.d.ts +77 -1
  30. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  31. package/dist/types/sdk/broker/index.d.ts +3 -0
  32. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  33. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  34. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  35. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  36. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  37. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  38. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  39. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  40. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  41. package/dist/types/sdk/bus/index.d.ts +6 -0
  42. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  43. package/dist/types/sdk/cli/index.d.ts +1 -0
  44. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  45. package/dist/types/sdk/cli/rows.d.ts +4 -3
  46. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  47. package/dist/types/sdk/client/client.d.ts +3 -2
  48. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  49. package/dist/types/sdk/host/control/index.d.ts +1 -1
  50. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  51. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  52. package/dist/types/sdk/host/host.d.ts +10 -0
  53. package/dist/types/sdk/host/session-runtime.d.ts +52 -0
  54. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  55. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  56. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  57. package/dist/types/sdk/router/session-router.d.ts +2 -0
  58. package/dist/types/sdk/session.d.ts +16 -0
  59. package/dist/types/sdk/startup-capability.d.ts +2 -1
  60. package/dist/types/session/agent-session.d.ts +5 -0
  61. package/dist/types/session/credential-store-errors.d.ts +1 -0
  62. package/dist/types/session/session-manager.d.ts +65 -0
  63. package/dist/types/session/yield-queue.d.ts +5 -4
  64. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  65. package/dist/types/tools/bash.d.ts +4 -0
  66. package/dist/types/tools/gh.d.ts +8 -0
  67. package/dist/types/tools/index.d.ts +4 -0
  68. package/package.json +7 -7
  69. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  71. package/src/cli/accounts-cli.ts +9 -5
  72. package/src/cli/args.ts +17 -0
  73. package/src/cli/fast-help.ts +1 -0
  74. package/src/cli/root-flags.ts +5 -0
  75. package/src/cli/update-cli.ts +77 -11
  76. package/src/commands/launch.ts +86 -18
  77. package/src/commands/sdk.ts +113 -21
  78. package/src/config/autorouting-tier-map.ts +12 -0
  79. package/src/config/file-lock.ts +158 -83
  80. package/src/config/model-preset-registry.ts +1 -5
  81. package/src/config/model-profiles.ts +5 -5
  82. package/src/config/model-registry.ts +23 -0
  83. package/src/config/model-resolver.ts +5 -0
  84. package/src/config/settings-schema.ts +8 -1
  85. package/src/config/settings.ts +5 -0
  86. package/src/coordinator-mcp/projection-scan.ts +21 -0
  87. package/src/coordinator-mcp/question-state.ts +9 -11
  88. package/src/coordinator-mcp/server.ts +94 -20
  89. package/src/crash/index-store.ts +2 -0
  90. package/src/crash/record-loader.ts +10 -0
  91. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  92. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  93. package/src/gjc-runtime/session-state-lock.ts +79 -6
  94. package/src/gjc-runtime/session-state-sidecar.ts +929 -23
  95. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  96. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  97. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  98. package/src/hooks/skill-state.ts +6 -0
  99. package/src/internal-urls/docs-index.generated.ts +7 -7
  100. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  101. package/src/main.ts +57 -2
  102. package/src/master-mode/context.ts +50 -0
  103. package/src/master-mode/first-request.ts +65 -0
  104. package/src/master-mode/snapshot.ts +205 -0
  105. package/src/modes/acp/acp-agent.ts +605 -156
  106. package/src/modes/components/footer.ts +4 -2
  107. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  108. package/src/modes/components/oauth-selector.ts +6 -1
  109. package/src/modes/components/settings-selector.ts +1 -0
  110. package/src/modes/components/status-line/model-name.ts +53 -0
  111. package/src/modes/components/status-line/priority-row.ts +107 -0
  112. package/src/modes/components/status-line/segments.ts +23 -9
  113. package/src/modes/components/tool-status-header.ts +151 -15
  114. package/src/modes/controllers/input-controller.ts +28 -0
  115. package/src/modes/controllers/selector-controller.ts +191 -96
  116. package/src/modes/print-mode.ts +64 -0
  117. package/src/modes/ui-language.ts +49 -2
  118. package/src/prompts/system/master-mode.md +28 -0
  119. package/src/sdk/broker/broker.ts +1636 -26
  120. package/src/sdk/broker/endpoint-authority.ts +86 -0
  121. package/src/sdk/broker/ensure.ts +5 -0
  122. package/src/sdk/broker/index.ts +3 -0
  123. package/src/sdk/broker/lifecycle.ts +186 -41
  124. package/src/sdk/broker/master-capability.ts +232 -0
  125. package/src/sdk/broker/session-index.ts +121 -17
  126. package/src/sdk/broker/session-scope.ts +272 -0
  127. package/src/sdk/broker/spawn-authority.ts +1031 -0
  128. package/src/sdk/broker/spawn-substrate.ts +548 -0
  129. package/src/sdk/broker/state-version.ts +26 -14
  130. package/src/sdk/broker/transport.ts +1 -0
  131. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  132. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  133. package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
  134. package/src/sdk/bus/index.ts +51 -4
  135. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  136. package/src/sdk/cli/index.ts +1 -0
  137. package/src/sdk/cli/master-cli.ts +259 -0
  138. package/src/sdk/cli/rows.ts +11 -8
  139. package/src/sdk/cli/session-cli.ts +264 -5
  140. package/src/sdk/client/client.ts +14 -8
  141. package/src/sdk/host/control/dispatch.ts +99 -38
  142. package/src/sdk/host/control/index.ts +2 -0
  143. package/src/sdk/host/control/operations.ts +2 -0
  144. package/src/sdk/host/control/runtime-gate.ts +203 -11
  145. package/src/sdk/host/host.ts +27 -2
  146. package/src/sdk/host/session-runtime.test.ts +210 -6
  147. package/src/sdk/host/session-runtime.ts +291 -49
  148. package/src/sdk/lifecycle/broker-client.ts +28 -0
  149. package/src/sdk/lifecycle/service.ts +279 -20
  150. package/src/sdk/lifecycle-session.ts +10 -2
  151. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  152. package/src/sdk/protocol/operation-registry.ts +13 -3
  153. package/src/sdk/router/session-router.ts +67 -59
  154. package/src/sdk/session.ts +357 -155
  155. package/src/sdk/startup-capability.ts +1 -0
  156. package/src/session/agent-session.ts +640 -103
  157. package/src/session/blob-store.ts +15 -5
  158. package/src/session/credential-store-errors.ts +2 -0
  159. package/src/session/internal/managed-session-storage.ts +153 -78
  160. package/src/session/session-manager.ts +337 -24
  161. package/src/session/yield-queue.ts +24 -10
  162. package/src/setup/frictionless-onboarding.ts +2 -0
  163. package/src/telemetry/events.ts +1138 -12
  164. package/src/tools/bash.ts +256 -42
  165. package/src/tools/gh.ts +104 -48
  166. package/src/tools/github-cache.ts +2 -2
  167. package/src/tools/index.ts +4 -0
@@ -4,6 +4,8 @@ import {
4
4
  type Api,
5
5
  type AuthCredentialSelector,
6
6
  type CredentialRemovalTarget,
7
+ isSqliteCorruptionError,
8
+ isSqliteError,
7
9
  type Model,
8
10
  resolveOAuthStorageProvider,
9
11
  } from "@gajae-code/ai/core";
@@ -106,6 +108,7 @@ import { TelegramDaemonController } from "../../sdk/bus/telegram-daemon-control"
106
108
  import { runTelegramSetup, type TelegramSetupPreflight } from "../../sdk/bus/telegram-setup";
107
109
  import { clearPersistentPinForRemovedRows } from "../../session/account-inventory";
108
110
  import type { DefaultFallbackRuntimeState } from "../../session/agent-session";
111
+ import { CREDENTIAL_STORE_UNREADABLE_MESSAGE } from "../../session/credential-store-errors";
109
112
  import { type SessionInfo, SessionManager } from "../../session/session-manager";
110
113
  import { getTreeForInternalRead } from "../../session/session-manager-internal";
111
114
  import { FileSessionStorage } from "../../session/session-storage";
@@ -125,6 +128,12 @@ import {
125
128
  logCredentialAutoImportFailures,
126
129
  runExternalCredentialAutoImport,
127
130
  } from "../../setup/credential-auto-import";
131
+
132
+ function credentialStoreOperationError(action: "Login" | "Logout", error: unknown): string {
133
+ const code = error && typeof error === "object" ? (error as { code?: unknown }).code : undefined;
134
+ return `${action} failed: credential store operation failed${typeof code === "string" ? ` (${code})` : ""}.`;
135
+ }
136
+
128
137
  import {
129
138
  filterAutoImportOAuthCredentials,
130
139
  formatDiscoverySummary,
@@ -172,6 +181,7 @@ import {
172
181
  FrictionlessOnboardingSelectorComponent,
173
182
  type FrictionlessOnboardingStage,
174
183
  getFrictionlessOnboardingCopy,
184
+ InterfaceLanguageSelectorComponent,
175
185
  } from "../components/frictionless-onboarding-selector";
176
186
  import type { PetMode } from "../components/gajae-pet-widget";
177
187
  import { HistorySearchComponent } from "../components/history-search";
@@ -216,6 +226,7 @@ import type { SessionObserverRegistry } from "../session-observer-registry";
216
226
  import { buildOAuthLoginAnchor, createOAuthUrlCopyLease } from "../shared/oauth-url-copy";
217
227
  import type { TasksAggregator } from "../tasks-aggregator";
218
228
  import type { TranscriptItemRegistry } from "../transcript-item-registry";
229
+ import { resolveExplicitUiLanguage, type UiLanguage } from "../ui-language";
219
230
  import { acquireResumeProgressLease, type ResumeProgressLease } from "../utils/ui-helpers";
220
231
 
221
232
  const CALLBACK_SERVER_PROVIDERS = new Set<string>([
@@ -1366,11 +1377,16 @@ export class SelectorController {
1366
1377
  async #refreshOAuthProviderAuthState(): Promise<void> {
1367
1378
  const oauthProviders = getOAuthProviders();
1368
1379
  await Promise.all(
1369
- oauthProviders.map(provider =>
1370
- this.ctx.session.modelRegistry
1371
- .getApiKeyForProvider(provider.id, this.ctx.session.credentialSessionId)
1372
- .catch(() => undefined),
1373
- ),
1380
+ oauthProviders.map(async provider => {
1381
+ try {
1382
+ await this.ctx.session.modelRegistry.getApiKeyForProvider(
1383
+ provider.id,
1384
+ this.ctx.session.credentialSessionId,
1385
+ );
1386
+ } catch (error) {
1387
+ if (isSqliteError(error)) throw error;
1388
+ }
1389
+ }),
1374
1390
  );
1375
1391
  }
1376
1392
  /**
@@ -1469,11 +1485,57 @@ export class SelectorController {
1469
1485
 
1470
1486
  async showFrictionlessOnboarding(): Promise<void> {
1471
1487
  const agentDir = this.ctx.session.getSessionAgentDir();
1488
+ const languagePreference = resolveExplicitUiLanguage(
1489
+ this.ctx.settings.has("ui.language") ? this.ctx.settings.get("ui.language") : undefined,
1490
+ process.env.GJC_UI_LANGUAGE,
1491
+ );
1492
+ let selectedLanguage: UiLanguage = languagePreference.language;
1493
+ if (!languagePreference.hasPreference && this.ctx.settings.canPersistDurableConfig()) {
1494
+ const decision = Promise.withResolvers<UiLanguage | undefined>();
1495
+ let settled = false;
1496
+ let unregisterStop: (() => void) | undefined;
1497
+ const finish = (language: UiLanguage | undefined): void => {
1498
+ if (settled) return;
1499
+ settled = true;
1500
+ unregisterStop?.();
1501
+ decision.resolve(language);
1502
+ };
1503
+ this.showSelector(done => {
1504
+ unregisterStop = this.ctx.onStop(() => {
1505
+ done();
1506
+ finish(undefined);
1507
+ });
1508
+ const selector = new InterfaceLanguageSelectorComponent(
1509
+ language => {
1510
+ done();
1511
+ finish(language);
1512
+ },
1513
+ () => {
1514
+ done();
1515
+ finish(undefined);
1516
+ },
1517
+ );
1518
+ return { component: selector, focus: selector };
1519
+ });
1520
+ selectedLanguage = (await decision.promise) ?? "en";
1521
+ try {
1522
+ await this.ctx.settings.commitAtomicBatch([{ path: "ui.language", op: "set", value: selectedLanguage }]);
1523
+ } catch (error) {
1524
+ this.ctx.showError(
1525
+ error instanceof Error
1526
+ ? `${error.message}; continuing in English.`
1527
+ : "Cannot save the interface language; continuing in English.",
1528
+ );
1529
+ selectedLanguage = "en";
1530
+ }
1531
+ } else if (!languagePreference.hasPreference) {
1532
+ this.ctx.showError("Cannot save the interface language; continuing in English.");
1533
+ }
1472
1534
  const presence = await discoverOnboardingRootPresence();
1473
1535
  // An explicit `/language` (or settings) selection outranks locale and transcript evidence.
1474
1536
  const profileOptions = {
1475
1537
  osLocale: Intl.DateTimeFormat().resolvedOptions().locale,
1476
- ...(this.ctx.settings.has("ui.language") ? { preferredLanguage: this.ctx.settings.get("ui.language") } : {}),
1538
+ preferredLanguage: selectedLanguage,
1477
1539
  };
1478
1540
  const initialProfile = deriveOnboardingProfile([], profileOptions);
1479
1541
 
@@ -3557,7 +3619,8 @@ export class SelectorController {
3557
3619
  }
3558
3620
  this.ctx.ui.requestRender();
3559
3621
  } catch (error: unknown) {
3560
- this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
3622
+ if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
3623
+ else this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
3561
3624
  } finally {
3562
3625
  oauthUrlCopyLease.release();
3563
3626
  if (useManualInput) {
@@ -3599,7 +3662,8 @@ export class SelectorController {
3599
3662
  `Successfully removed ${result.ids.length} stored credential${result.ids.length === 1 ? "" : "s"} from ${providerId}.`,
3600
3663
  );
3601
3664
  } catch (error: unknown) {
3602
- this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
3665
+ if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
3666
+ else this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
3603
3667
  }
3604
3668
  }
3605
3669
 
@@ -3616,75 +3680,90 @@ export class SelectorController {
3616
3680
  return;
3617
3681
  }
3618
3682
  const authStorage = this.ctx.session.modelRegistry.authStorage;
3619
- const hasOAuthInventory = authStorage
3620
- .listCredentialInventory(selectedProviderId)
3621
- .some(row => row.provider === selectedProviderId && row.credentialKind === "oauth");
3622
- const hasRemovableCredential = authStorage
3623
- .listCredentialRemovalTargets(selectedProviderId)
3624
- .some(target => target.provider === selectedProviderId);
3625
- if (mode === "logout" && hasOAuthInventory && !hasRemovableCredential) {
3626
- await this.#handleLogout(selectedProviderId);
3627
- return;
3628
- }
3629
- if (hasOAuthInventory && !options?.manualCode) {
3630
- this.showSelector(done => {
3631
- let selector: OAuthSelectorComponent;
3632
- selector = new OAuthSelectorComponent(
3633
- mode,
3634
- authStorage,
3635
- () => undefined,
3636
- () => {
3637
- selector.stopValidation();
3638
- done();
3639
- this.ctx.ui.requestRender();
3640
- },
3641
- {
3642
- accountProviderId: selectedProviderId,
3643
- onAccountSelect: async (selectorValue: AuthCredentialSelector) => {
3644
- await this.ctx.session.setCredentialPin(selectedProviderId, selectorValue);
3645
- selector.stopValidation();
3646
- done();
3647
- this.ctx.showStatus(`Pinned OAuth account for ${selectedProviderId} to this session.`);
3648
- },
3649
- onAutoSelect: async () => {
3650
- await this.ctx.session.setCredentialAuto(selectedProviderId);
3651
- selector.stopValidation();
3652
- done();
3653
- this.ctx.showStatus(`Using AUTO (ranked) OAuth accounts for ${selectedProviderId}.`);
3654
- },
3655
- onAddAccount: async () => {
3683
+ try {
3684
+ const hasOAuthInventory = authStorage
3685
+ .listCredentialInventory(selectedProviderId)
3686
+ .some(row => row.provider === selectedProviderId && row.credentialKind === "oauth");
3687
+ const hasRemovableCredential = authStorage
3688
+ .listCredentialRemovalTargets(selectedProviderId)
3689
+ .some(target => target.provider === selectedProviderId);
3690
+ if (mode === "logout" && hasOAuthInventory && !hasRemovableCredential) {
3691
+ await this.#handleLogout(selectedProviderId);
3692
+ return;
3693
+ }
3694
+ if (hasOAuthInventory && !options?.manualCode) {
3695
+ this.showSelector(done => {
3696
+ let selector: OAuthSelectorComponent;
3697
+ selector = new OAuthSelectorComponent(
3698
+ mode,
3699
+ authStorage,
3700
+ () => undefined,
3701
+ () => {
3656
3702
  selector.stopValidation();
3657
3703
  done();
3658
- await this.#handleOAuthLogin(selectedProviderId, options);
3704
+ this.ctx.ui.requestRender();
3659
3705
  },
3660
- onAccountRemove: async targets => {
3661
- await this.#handleLogout(selectedProviderId, targets);
3662
- selector.stopValidation();
3663
- done();
3706
+ {
3707
+ accountProviderId: selectedProviderId,
3708
+ onAccountSelect: async (selectorValue: AuthCredentialSelector) => {
3709
+ await this.ctx.session.setCredentialPin(selectedProviderId, selectorValue);
3710
+ selector.stopValidation();
3711
+ done();
3712
+ this.ctx.showStatus(`Pinned OAuth account for ${selectedProviderId} to this session.`);
3713
+ },
3714
+ onAutoSelect: async () => {
3715
+ await this.ctx.session.setCredentialAuto(selectedProviderId);
3716
+ selector.stopValidation();
3717
+ done();
3718
+ this.ctx.showStatus(`Using AUTO (ranked) OAuth accounts for ${selectedProviderId}.`);
3719
+ },
3720
+ onAddAccount: async () => {
3721
+ selector.stopValidation();
3722
+ done();
3723
+ await this.#handleOAuthLogin(selectedProviderId, options);
3724
+ },
3725
+ onAccountRemove: async targets => {
3726
+ await this.#handleLogout(selectedProviderId, targets);
3727
+ selector.stopValidation();
3728
+ done();
3729
+ },
3664
3730
  },
3665
- },
3666
- );
3667
- return { component: selector, focus: selector };
3668
- });
3731
+ );
3732
+ return { component: selector, focus: selector };
3733
+ });
3734
+ return;
3735
+ }
3736
+ if (mode === "login") {
3737
+ await this.#handleOAuthLogin(selectedProviderId, options);
3738
+ return;
3739
+ }
3740
+ // mode === "logout" with no OAuth accounts: remove stored api-key credentials directly.
3741
+ await this.#handleLogout(selectedProviderId);
3669
3742
  return;
3670
- }
3671
- if (mode === "login") {
3672
- await this.#handleOAuthLogin(selectedProviderId, options);
3743
+ } catch (error: unknown) {
3744
+ if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
3745
+ else if (isSqliteError(error))
3746
+ this.ctx.showError(credentialStoreOperationError(mode === "login" ? "Login" : "Logout", error));
3747
+ else throw error;
3673
3748
  return;
3674
3749
  }
3675
- // mode === "logout" with no OAuth accounts: remove stored api-key credentials directly.
3676
- await this.#handleLogout(selectedProviderId);
3677
- return;
3678
3750
  }
3679
3751
 
3680
3752
  if (mode === "logout") {
3681
- await this.#refreshOAuthProviderAuthState();
3682
- const oauthProviders = getOAuthProviders();
3683
- const loggedInProviders = oauthProviders.filter(provider =>
3684
- this.ctx.session.modelRegistry.authStorage.hasAuth(provider.id),
3685
- );
3686
- if (loggedInProviders.length === 0) {
3687
- this.ctx.showStatus("No OAuth providers logged in. Use /login first.");
3753
+ try {
3754
+ await this.#refreshOAuthProviderAuthState();
3755
+ const oauthProviders = getOAuthProviders();
3756
+ const loggedInProviders = oauthProviders.filter(provider =>
3757
+ this.ctx.session.modelRegistry.authStorage.hasAuth(provider.id),
3758
+ );
3759
+ if (loggedInProviders.length === 0) {
3760
+ this.ctx.showStatus("No OAuth providers logged in. Use /login first.");
3761
+ return;
3762
+ }
3763
+ } catch (error: unknown) {
3764
+ if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
3765
+ else if (isSqliteError(error)) this.ctx.showError(credentialStoreOperationError("Logout", error));
3766
+ else throw error;
3688
3767
  return;
3689
3768
  }
3690
3769
  }
@@ -3807,37 +3886,53 @@ export class SelectorController {
3807
3886
  }
3808
3887
  }
3809
3888
  }
3810
- this.showSelector(done => {
3811
- let selector: OAuthSelectorComponent;
3812
- selector = new OAuthSelectorComponent(
3813
- mode,
3814
- this.ctx.session.modelRegistry.authStorage,
3815
- async (selectedProviderId: string) => {
3816
- selector.stopValidation();
3817
- done();
3818
- await this.showOAuthSelector(mode, selectedProviderId);
3819
- },
3820
- () => {
3821
- selector.stopValidation();
3822
- done();
3823
- this.ctx.ui.requestLayoutRender("oauth-selector-close");
3824
- },
3825
- {
3826
- validateAuth: async (selectedProviderId: string) => {
3827
- const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider(
3828
- selectedProviderId,
3829
- this.ctx.session.credentialSessionId,
3830
- );
3831
- return !!apiKey;
3889
+ try {
3890
+ this.showSelector(done => {
3891
+ let selector: OAuthSelectorComponent;
3892
+ selector = new OAuthSelectorComponent(
3893
+ mode,
3894
+ this.ctx.session.modelRegistry.authStorage,
3895
+ async (selectedProviderId: string) => {
3896
+ selector.stopValidation();
3897
+ done();
3898
+ await this.showOAuthSelector(mode, selectedProviderId);
3832
3899
  },
3833
- requestRender: () => {
3834
- this.ctx.ui.requestLayoutRender("oauth-selector-spinner");
3900
+ () => {
3901
+ selector.stopValidation();
3902
+ done();
3903
+ this.ctx.ui.requestLayoutRender("oauth-selector-close");
3835
3904
  },
3836
- externalCredentialCandidates,
3837
- },
3838
- );
3839
- return { component: selector, focus: selector };
3840
- });
3905
+ {
3906
+ validateAuth: async (selectedProviderId: string) => {
3907
+ const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider(
3908
+ selectedProviderId,
3909
+ this.ctx.session.credentialSessionId,
3910
+ );
3911
+ return !!apiKey;
3912
+ },
3913
+ onValidationError: error => {
3914
+ if (!isSqliteError(error)) return false;
3915
+ selector.stopValidation();
3916
+ done();
3917
+ this.ctx.showError(
3918
+ isSqliteCorruptionError(error)
3919
+ ? CREDENTIAL_STORE_UNREADABLE_MESSAGE
3920
+ : credentialStoreOperationError("Login", error),
3921
+ );
3922
+ return true;
3923
+ },
3924
+ requestRender: () => {
3925
+ this.ctx.ui.requestLayoutRender("oauth-selector-spinner");
3926
+ },
3927
+ externalCredentialCandidates,
3928
+ },
3929
+ );
3930
+ return { component: selector, focus: selector };
3931
+ });
3932
+ } catch (error: unknown) {
3933
+ if (!isSqliteCorruptionError(error)) throw error;
3934
+ this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
3935
+ }
3841
3936
  }
3842
3937
 
3843
3938
  showDebugSelector(): void {
@@ -5,6 +5,7 @@
5
5
  * - `gjc -p "prompt"` - text output
6
6
  * - `gjc --mode json "prompt"` - JSON event stream
7
7
  */
8
+ import type { AgentMessage } from "@gajae-code/agent-core";
8
9
  import { type AssistantMessage, type ImageContent, isContextOverflow } from "@gajae-code/ai/core";
9
10
  import { isKnownSinkPeerClosedError, logger, sanitizeText } from "@gajae-code/utils";
10
11
  import { loadSlashCommands } from "../extensibility/slash-commands";
@@ -46,6 +47,48 @@ export interface PrintModeOptions {
46
47
  */
47
48
  export const CONTEXT_OVERFLOW_EXIT_CODE = 78;
48
49
 
50
+ const EMPTY_TURN_ERROR = {
51
+ code: "empty_response",
52
+ message: "Turn completed without assistant text.",
53
+ } as const;
54
+
55
+ export type PrintTerminalDisposition =
56
+ | { status: "success"; assistantText: true }
57
+ | { status: "failure"; error: typeof EMPTY_TURN_ERROR | { code: "assistant_error"; message: string } };
58
+
59
+ function assistantText(message: AssistantMessage): string {
60
+ return message.content
61
+ .filter((content): content is { type: "text"; text: string } => content.type === "text")
62
+ .map(content => content.text)
63
+ .join("");
64
+ }
65
+
66
+ export function resolvePrintTerminalDisposition(
67
+ messages: readonly AgentMessage[],
68
+ submittedPrompt: boolean,
69
+ turnStartIndex?: number,
70
+ ): PrintTerminalDisposition | undefined {
71
+ if (!submittedPrompt) return undefined;
72
+ const turnMessages = turnStartIndex === undefined ? messages : messages.slice(turnStartIndex);
73
+ const lastAssistant = turnMessages.findLast((message): message is AssistantMessage => message.role === "assistant");
74
+ if (!lastAssistant) return { status: "failure", error: EMPTY_TURN_ERROR };
75
+ if (
76
+ (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted") &&
77
+ !isSilentAbort(lastAssistant.errorMessage)
78
+ ) {
79
+ return {
80
+ status: "failure",
81
+ error: {
82
+ code: "assistant_error",
83
+ message: sanitizeText(lastAssistant.errorMessage || `Request ${lastAssistant.stopReason}`),
84
+ },
85
+ };
86
+ }
87
+ if (isSilentAbort(lastAssistant.errorMessage)) return undefined;
88
+ if (assistantText(lastAssistant).trim().length === 0) return { status: "failure", error: EMPTY_TURN_ERROR };
89
+ return { status: "success", assistantText: true };
90
+ }
91
+
49
92
  /**
50
93
  * Build an actionable stderr diagnostic for a terminal context-overflow error in
51
94
  * text mode. The raw provider message is preserved (appended) for debugging, but
@@ -201,6 +244,8 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
201
244
  let unsubscribe: (() => void) | undefined;
202
245
  let submittedPrompt = false;
203
246
  let consumedCommand = false;
247
+ let jsonFailureObserved = false;
248
+ let lastPromptMessageStart: number | undefined;
204
249
 
205
250
  try {
206
251
  for (const warning of new Set(session.configWarnings)) {
@@ -237,6 +282,14 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
237
282
  // when it must render the JSON event stream.
238
283
  if (mode === "json") {
239
284
  unsubscribe = session.subscribe(event => {
285
+ if (
286
+ event.type === "agent_failed" ||
287
+ (event.type === "message_end" &&
288
+ event.message.role === "assistant" &&
289
+ (event.message.stopReason === "error" || event.message.stopReason === "aborted"))
290
+ ) {
291
+ jsonFailureObserved = true;
292
+ }
240
293
  stdout.write(`${JSON.stringify(event)}\n`);
241
294
  });
242
295
  }
@@ -265,6 +318,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
265
318
  if (command !== false) text = command.prompt;
266
319
  }
267
320
  submittedPrompt = true;
321
+ lastPromptMessageStart = session.state.messages.length;
268
322
  await session.prompt(text, images ? { images } : undefined);
269
323
  };
270
324
 
@@ -277,6 +331,16 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
277
331
  await logger.time("print:prompt:next", () => submitInput(message));
278
332
  }
279
333
 
334
+ const terminal = resolvePrintTerminalDisposition(session.state.messages, submittedPrompt, lastPromptMessageStart);
335
+ if (terminal?.status === "failure") {
336
+ if (!options.suppressProcessExit) process.exitCode = 1;
337
+ if (mode === "json" && !jsonFailureObserved) {
338
+ stdout.write(`${JSON.stringify({ type: "agent_failed", error: terminal.error })}\n`);
339
+ } else if (mode === "text" && terminal.error.code === EMPTY_TURN_ERROR.code) {
340
+ await writeStderrAndQuiesce(`${terminal.error.message}\n`);
341
+ }
342
+ }
343
+
280
344
  // In text mode, output final response.
281
345
  if (mode === "text" && (!consumedCommand || submittedPrompt)) {
282
346
  const lastMessage = session.state.messages.findLast(message => message.role === "assistant");
@@ -1,4 +1,4 @@
1
- export const UI_LANGUAGES = ["en", "ko", "ja"] as const;
1
+ export const UI_LANGUAGES = ["en", "ko", "zh", "ja"] as const;
2
2
 
3
3
  export type UiLanguage = (typeof UI_LANGUAGES)[number];
4
4
 
@@ -23,6 +23,7 @@ const ENGLISH_STRINGS = {
23
23
  "settings.language.description": "Language for human-facing interactive UI text",
24
24
  "settings.language.english": "English",
25
25
  "settings.language.korean": "Korean (한국어)",
26
+ "settings.language.chinese": "Chinese (简体中文)",
26
27
  "settings.language.japanese": "Japanese (日本語)",
27
28
  "language.current": "Current UI language:",
28
29
  "language.changed": "UI language changed to",
@@ -52,6 +53,7 @@ const KOREAN_STRINGS: Record<UiStringKey, string> = {
52
53
  "settings.language.description": "사람이 읽는 대화형 UI 텍스트의 언어",
53
54
  "settings.language.english": "English",
54
55
  "settings.language.korean": "한국어",
56
+ "settings.language.chinese": "简体中文",
55
57
  "settings.language.japanese": "日本語",
56
58
  "language.current": "현재 UI 언어:",
57
59
  "language.changed": "UI 언어를 다음으로 변경했습니다:",
@@ -79,21 +81,61 @@ const JAPANESE_STRINGS: Record<UiStringKey, string> = {
79
81
  "settings.language.description": "対話型 UI テキストに使う言語",
80
82
  "settings.language.english": "English",
81
83
  "settings.language.korean": "한국어",
84
+ "settings.language.chinese": "简体中文",
82
85
  "settings.language.japanese": "日本語",
83
86
  "language.current": "現在の UI 言語:",
84
87
  "language.changed": "UI 言語を次に変更しました:",
85
88
  "language.unknown": "不明な言語",
86
89
  };
87
90
 
91
+ const CHINESE_STRINGS: Record<UiStringKey, string> = {
92
+ "settings.title": "设置",
93
+ "settings.navigationHint": "(按 Tab 切换)",
94
+ "settings.selectHint": " Enter:选择 · Esc:返回",
95
+ "settings.preview": "预览:",
96
+ "settings.tab.appearance": "外观",
97
+ "settings.tab.model": "模型",
98
+ "settings.tab.interaction": "交互",
99
+ "settings.tab.context": "上下文",
100
+ "settings.tab.memory": "记忆",
101
+ "settings.tab.editing": "编辑",
102
+ "settings.tab.tools": "工具",
103
+ "settings.tab.tasks": "任务",
104
+ "settings.tab.providers": "提供商",
105
+ "settings.tab.notifications": "通知",
106
+ "settings.tab.plugins": "插件",
107
+ "settings.tab.gjcBundles": "GJC 捆绑包",
108
+ "settings.language.label": "语言",
109
+ "settings.language.description": "面向用户的交互式界面文字所使用的语言",
110
+ "settings.language.english": "English",
111
+ "settings.language.korean": "한국어",
112
+ "settings.language.chinese": "简体中文",
113
+ "settings.language.japanese": "日本語",
114
+ "language.current": "当前界面语言:",
115
+ "language.changed": "界面语言已更改为",
116
+ "language.unknown": "未知语言",
117
+ };
118
+
88
119
  const STRINGS: Record<UiLanguage, Record<UiStringKey, string>> = {
89
120
  en: ENGLISH_STRINGS,
90
121
  ko: KOREAN_STRINGS,
122
+ zh: CHINESE_STRINGS,
91
123
  ja: JAPANESE_STRINGS,
92
124
  };
93
125
 
94
126
  /** User selection is authoritative; invalid or unavailable values deterministically fall back to English. */
95
127
  export function resolveUiLanguage(value: unknown): UiLanguage {
96
- return value === "ko" || value === "ja" ? value : "en";
128
+ return value === "ko" || value === "zh" || value === "ja" ? value : "en";
129
+ }
130
+
131
+ /** Resolve explicit startup preference without allowing an invalid value to block startup. */
132
+ export function resolveExplicitUiLanguage(
133
+ configuredValue: unknown,
134
+ environmentValue: string | undefined,
135
+ ): { language: UiLanguage; hasPreference: boolean } {
136
+ if (configuredValue !== undefined) return { language: resolveUiLanguage(configuredValue), hasPreference: true };
137
+ if (environmentValue === undefined) return { language: "en", hasPreference: false };
138
+ return { language: parseUiLanguage(environmentValue) ?? "en", hasPreference: true };
97
139
  }
98
140
 
99
141
  export function uiString(language: unknown, key: UiStringKey): string {
@@ -104,6 +146,7 @@ export function uiString(language: unknown, key: UiStringKey): string {
104
146
  export const UI_LANGUAGE_LABELS: Record<UiLanguage, string> = {
105
147
  en: "English",
106
148
  ko: "한국어",
149
+ zh: "简体中文",
107
150
  ja: "日本語",
108
151
  };
109
152
 
@@ -117,6 +160,10 @@ const UI_LANGUAGE_ALIASES: Readonly<Record<string, UiLanguage>> = {
117
160
  kor: "ko",
118
161
  korean: "ko",
119
162
  한국어: "ko",
163
+ zh: "zh",
164
+ 中文: "zh",
165
+ 简体中文: "zh",
166
+ chinese: "zh",
120
167
  ja: "ja",
121
168
  jp: "ja",
122
169
  jpn: "ja",
@@ -0,0 +1,28 @@
1
+ # Master Mode
2
+
3
+ You are running as a GJC master session. You coordinate peer GJC sessions through the local Broker; you never control them directly.
4
+
5
+ ## Discovery and scope
6
+
7
+ - Your launch scope (`repo`, `pwd`, or `global`) bounds which peers you see. Every search result states its requested and resolved scope; trust that envelope, never assume wider visibility.
8
+ - Use `gjc sdk search [--scope repo|pwd|global] [--json]` for explicit scope-filtered discovery. Rows carry index live evidence plus a bounded probe verdict (`reachable`, `unreachable`, `stale`).
9
+ - A `not-in-git-worktree` result is a successful empty answer for `repo` scope outside a Git worktree; it never falls back to another scope.
10
+ - The one-time `<gjc-master-peer-snapshot>` block in your first request is a startup observation, not a live view; re-run `gjc sdk search` before acting on peers.
11
+
12
+ ## Peer orchestration
13
+
14
+ - All peer control is broker-routed and best effort: inspect, status, send, tail, steer, and interrupt through the `gjc sdk session` commands. Pre-existing peers owe you nothing; treat them as independent owners.
15
+ - Read-only first: inspect and observe a peer before sending steering or interrupts.
16
+ - Never steer a peer while its user is mid-turn; wait for the peer to be idle or explicitly awaiting input.
17
+ - Do not invent locks, reservations, or exclusive claims over peers; no such mechanism exists, and pretending otherwise corrupts coordination.
18
+
19
+ ## Spawning children
20
+
21
+ - `gjc sdk spawn --cwd <dir> --prompt <task> [--model <selector>] [--profile <name>] [--idempotency-key <key>]` creates one task-seeded background child through the Broker. It is legal only inside this interactive master session.
22
+ - One idempotency identity produces at most one child and one seed prompt. Reuse of the same key replays the stored outcome; a semantically new task requires a new invocation.
23
+ - Spawn responses carry only safe facts (claim, child session id, substrate kind, seed phase/status). `spawn_in_progress` and `terminal_uncertain` are honest states: never retry them blindly; inspect with `gjc sdk search` / session status instead. For `uncertain_after_send`, repeat the same spawn invocation with its reported `--idempotency-key` to join the original claim rather than create another child.
24
+
25
+ ## Delegation discipline
26
+
27
+ - Give each spawned child one bounded task with explicit acceptance criteria in the seed prompt; monitor via broker-routed status/tail and aggregate results yourself.
28
+ - Clean up children you created with `gjc sdk session raw global --op session.close --json-input '{"sessionId":"<id>"}' --idempotency-key <key>` when their work is integrated. Orphaned children are reaped automatically after the configured grace period, but explicit close is the polite default.