@gajae-code/coding-agent 0.4.4 → 0.5.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 (132) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/types/cli/fast-help.d.ts +1 -0
  3. package/dist/types/cli/setup-cli.d.ts +2 -0
  4. package/dist/types/commands/harness.d.ts +6 -0
  5. package/dist/types/commands/setup.d.ts +6 -0
  6. package/dist/types/config/model-profile-activation.d.ts +11 -2
  7. package/dist/types/config/model-profiles.d.ts +7 -0
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/model-resolver.d.ts +2 -0
  10. package/dist/types/config/models-config-schema.d.ts +35 -0
  11. package/dist/types/config/settings-schema.d.ts +4 -3
  12. package/dist/types/coordinator/contract.d.ts +1 -1
  13. package/dist/types/coordinator-mcp/server.d.ts +8 -2
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  16. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +1 -1
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  21. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  22. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  23. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  24. package/dist/types/harness-control-plane/types.d.ts +13 -1
  25. package/dist/types/hindsight/mental-models.d.ts +5 -5
  26. package/dist/types/main.d.ts +2 -2
  27. package/dist/types/modes/components/model-selector.d.ts +1 -12
  28. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  29. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  30. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  31. package/dist/types/sdk.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +2 -0
  33. package/dist/types/session/blob-store.d.ts +20 -1
  34. package/dist/types/session/session-manager.d.ts +32 -6
  35. package/dist/types/session/streaming-output.d.ts +3 -2
  36. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  37. package/dist/types/setup/hermes-setup.d.ts +7 -0
  38. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  39. package/dist/types/task/receipt.d.ts +2 -0
  40. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  41. package/dist/types/task/types.d.ts +17 -0
  42. package/dist/types/thinking-metadata.d.ts +16 -0
  43. package/dist/types/thinking.d.ts +3 -12
  44. package/dist/types/tools/index.d.ts +2 -0
  45. package/dist/types/tools/resolve.d.ts +0 -10
  46. package/dist/types/utils/tool-choice.d.ts +14 -1
  47. package/package.json +8 -7
  48. package/scripts/build-binary.ts +4 -0
  49. package/src/cli/fast-help.ts +80 -0
  50. package/src/cli/setup-cli.ts +12 -3
  51. package/src/cli.ts +112 -17
  52. package/src/commands/coordinator.ts +44 -1
  53. package/src/commands/harness.ts +128 -11
  54. package/src/commands/launch.ts +2 -2
  55. package/src/commands/mcp-serve.ts +3 -2
  56. package/src/commands/session.ts +3 -1
  57. package/src/commands/setup.ts +4 -0
  58. package/src/config/model-profile-activation.ts +15 -3
  59. package/src/config/model-profiles.ts +255 -56
  60. package/src/config/model-resolver.ts +9 -6
  61. package/src/config/models-config-schema.ts +2 -0
  62. package/src/config/settings-schema.ts +6 -3
  63. package/src/coordinator/contract.ts +1 -0
  64. package/src/coordinator-mcp/server.ts +427 -193
  65. package/src/cursor.ts +46 -4
  66. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  67. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  68. package/src/export/html/index.ts +13 -9
  69. package/src/gjc-runtime/launch-worktree.ts +12 -1
  70. package/src/gjc-runtime/session-state-sidecar.ts +38 -0
  71. package/src/gjc-runtime/team-runtime.ts +33 -7
  72. package/src/gjc-runtime/tmux-common.ts +15 -0
  73. package/src/gjc-runtime/tmux-sessions.ts +19 -11
  74. package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
  75. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  76. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  77. package/src/harness-control-plane/finalize.ts +39 -5
  78. package/src/harness-control-plane/owner.ts +87 -28
  79. package/src/harness-control-plane/phase-rollup.ts +96 -0
  80. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  81. package/src/harness-control-plane/receipt-spool.ts +128 -0
  82. package/src/harness-control-plane/receipts.ts +229 -1
  83. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  84. package/src/harness-control-plane/state-machine.ts +27 -6
  85. package/src/harness-control-plane/storage.ts +23 -0
  86. package/src/harness-control-plane/types.ts +33 -1
  87. package/src/hindsight/mental-models.ts +17 -16
  88. package/src/internal-urls/docs-index.generated.ts +8 -7
  89. package/src/main.ts +7 -3
  90. package/src/modes/components/assistant-message.ts +26 -14
  91. package/src/modes/components/diff.ts +97 -0
  92. package/src/modes/components/model-selector.ts +353 -181
  93. package/src/modes/components/status-line.ts +6 -6
  94. package/src/modes/components/tool-execution.ts +30 -13
  95. package/src/modes/controllers/event-controller.ts +5 -4
  96. package/src/modes/controllers/selector-controller.ts +33 -42
  97. package/src/modes/interactive-mode.ts +4 -5
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +3 -2
  100. package/src/modes/rpc/rpc-mode.ts +44 -14
  101. package/src/modes/rpc/rpc-types.ts +5 -2
  102. package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
  103. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  104. package/src/modes/theme/theme.ts +2 -2
  105. package/src/modes/utils/abort-message.ts +41 -0
  106. package/src/modes/utils/context-usage.ts +15 -8
  107. package/src/modes/utils/ui-helpers.ts +5 -6
  108. package/src/sdk.ts +38 -6
  109. package/src/secrets/obfuscator.ts +102 -27
  110. package/src/session/agent-session.ts +121 -25
  111. package/src/session/blob-store.ts +89 -3
  112. package/src/session/session-manager.ts +328 -57
  113. package/src/session/streaming-output.ts +185 -122
  114. package/src/session/tool-choice-queue.ts +23 -0
  115. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
  116. package/src/setup/hermes-setup.ts +63 -8
  117. package/src/task/executor.ts +69 -6
  118. package/src/task/fork-context-advisory.ts +99 -0
  119. package/src/task/index.ts +31 -2
  120. package/src/task/receipt.ts +7 -0
  121. package/src/task/render.ts +21 -1
  122. package/src/task/roi-reconciliation.ts +90 -0
  123. package/src/task/types.ts +15 -0
  124. package/src/thinking-metadata.ts +51 -0
  125. package/src/thinking.ts +26 -46
  126. package/src/tools/bash.ts +1 -1
  127. package/src/tools/index.ts +4 -2
  128. package/src/tools/resolve.ts +93 -18
  129. package/src/tools/subagent-render.ts +10 -1
  130. package/src/utils/edit-mode.ts +1 -1
  131. package/src/utils/title-generator.ts +16 -2
  132. package/src/utils/tool-choice.ts +45 -16
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { AgentMessage } from "@gajae-code/agent-core";
3
- import { estimateTokens } from "@gajae-code/agent-core/compaction";
3
+ import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
4
4
  import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
5
5
  import { formatCount, getProjectDir } from "@gajae-code/utils";
6
6
  import { $ } from "bun";
@@ -50,7 +50,7 @@ export interface StatusLineSettings {
50
50
 
51
51
  /**
52
52
  * Symbol-keyed sidecar tagged onto each `AgentMessage` to memoize its
53
- * `estimateTokens` result. Keyed by message identity (the object itself);
53
+ * `estimateMessageTokensHeuristic` result. Keyed by message identity (the object itself);
54
54
  * a cheap content fingerprint detects in-place mutations (post-hoc error
55
55
  * attachment, retry-truncated branch rebuild, etc.) and forces recompute.
56
56
  *
@@ -64,11 +64,11 @@ interface TaggedMessage {
64
64
  }
65
65
 
66
66
  /**
67
- * Cheap structural fingerprint mirroring `estimateTokens`'s content walk.
67
+ * Cheap structural fingerprint mirroring `estimateMessageTokensHeuristic`'s content walk.
68
68
  * O(blocks) — only reads string `.length` and primitives, never copies or
69
69
  * serializes content. Any in-place mutation that alters total tokenized
70
70
  * content also alters one of the byte-length sums or block counts captured
71
- * here, forcing the cached `estimateTokens` value to be recomputed.
71
+ * here, forcing the cached heuristic token value to be recomputed.
72
72
  */
73
73
  function messageFingerprint(msg: AgentMessage): string {
74
74
  const role = (msg as { role?: string }).role ?? "";
@@ -136,7 +136,7 @@ function tokensForMessage(msg: AgentMessage): number {
136
136
  const tagged = msg as TaggedMessage;
137
137
  const cached = tagged[kTokenCache];
138
138
  if (cached && cached.fingerprint === fp) return cached.tokens;
139
- const tokens = estimateTokens(msg);
139
+ const tokens = estimateMessageTokensHeuristic(msg);
140
140
  tagged[kTokenCache] = { fingerprint: fp, tokens };
141
141
  return tokens;
142
142
  }
@@ -560,7 +560,7 @@ export class StatusLineComponent implements Component {
560
560
  let messagesTokens = 0;
561
561
  const lastIdx = messages.length - 1;
562
562
  for (let i = 0; i < messages.length; i++) {
563
- messagesTokens += i === lastIdx ? estimateTokens(messages[i]) : tokensForMessage(messages[i]);
563
+ messagesTokens += i === lastIdx ? estimateMessageTokensHeuristic(messages[i]) : tokensForMessage(messages[i]);
564
564
  }
565
565
 
566
566
  const usedTokens = this.#nonMessageTokensCache + messagesTokens;
@@ -152,6 +152,7 @@ export class ToolExecutionComponent extends Container {
152
152
  isError?: boolean;
153
153
  details?: any;
154
154
  };
155
+ #textOutputCache?: { content: unknown; showImages: boolean; terminalImageProtocol: unknown; output: string };
155
156
  // Edit preview state
156
157
  #editMode?: EditMode;
157
158
  #editDiffPreview?: PerFileDiffPreview[];
@@ -197,9 +198,11 @@ export class ToolExecutionComponent extends Container {
197
198
 
198
199
  this.addChild(new Spacer(1));
199
200
 
200
- // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
201
- this.#contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
202
- this.#contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
201
+ // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
202
+ // Vertical padding is 0: block separation comes solely from the leading Spacer
203
+ // (1 blank line above each block), matching reference TUIs (083.2).
204
+ this.#contentBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
205
+ this.#contentText = new Text("", 1, 0, (text: string) => theme.bg("toolPendingBg", text));
203
206
 
204
207
  // Use Box for custom tools or built-in tools that have renderers
205
208
  const hasRenderer = toolName in toolRenderers;
@@ -295,6 +298,7 @@ export class ToolExecutionComponent extends Container {
295
298
  isPartial = false,
296
299
  _toolCallId?: string,
297
300
  ): void {
301
+ this.#textOutputCache = undefined;
298
302
  this.#result = result;
299
303
  this.#isPartial = isPartial;
300
304
  // When tool is complete, ensure args are marked complete so spinner stops
@@ -397,6 +401,7 @@ export class ToolExecutionComponent extends Container {
397
401
 
398
402
  setShowImages(show: boolean): void {
399
403
  this.#showImages = show;
404
+ this.#textOutputCache = undefined;
400
405
  this.#updateDisplay();
401
406
  }
402
407
 
@@ -514,7 +519,7 @@ export class ToolExecutionComponent extends Container {
514
519
  const fileBgFn = fileResult.isError
515
520
  ? (text: string) => theme.bg("toolErrorBg", text)
516
521
  : (text: string) => theme.bg("toolSuccessBg", text);
517
- const fileBox = new Box(1, 1, fileBgFn);
522
+ const fileBox = new Box(1, 0, fileBgFn);
518
523
  try {
519
524
  const resultComponent = renderer.renderResult(
520
525
  { content: [], details: fileResult, isError: fileResult.isError },
@@ -540,7 +545,7 @@ export class ToolExecutionComponent extends Container {
540
545
  const pendingSpacer = new Spacer(1);
541
546
  this.#multiFileBoxes.push(pendingSpacer);
542
547
  this.addChild(pendingSpacer);
543
- const pendingBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
548
+ const pendingBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
544
549
  const pendingText = renderStatusLine(
545
550
  {
546
551
  icon: "pending",
@@ -723,16 +728,27 @@ export class ToolExecutionComponent extends Container {
723
728
  #getTextOutput(): string {
724
729
  if (!this.#result) return "";
725
730
 
726
- const textBlocks = this.#result.content?.filter((c: any) => c.type === "text") || [];
727
- const imageBlocks = this.#getAllImageBlocks();
731
+ const content = this.#result.content;
732
+ const terminalImageProtocol = TERMINAL.imageProtocol;
733
+ const cached = this.#textOutputCache;
734
+ if (
735
+ cached?.content === content &&
736
+ cached.showImages === this.#showImages &&
737
+ cached.terminalImageProtocol === terminalImageProtocol
738
+ ) {
739
+ return cached.output;
740
+ }
728
741
 
729
- let output = textBlocks
730
- .map((c: any) => {
731
- return sanitizeWithOptionalSixelPassthrough(c.text || "", sanitizeText);
732
- })
733
- .join("\n");
742
+ const textParts: string[] = [];
743
+ for (const block of content ?? []) {
744
+ if (block.type !== "text") continue;
745
+ const text = block.text || "";
746
+ textParts.push(sanitizeWithOptionalSixelPassthrough(text, sanitizeText));
747
+ }
748
+ let output = textParts.join("\n");
734
749
 
735
- if (imageBlocks.length > 0 && (!TERMINAL.imageProtocol || !this.#showImages)) {
750
+ const imageBlocks = this.#getAllImageBlocks();
751
+ if (imageBlocks.length > 0 && (!terminalImageProtocol || !this.#showImages)) {
736
752
  const imageIndicators = imageBlocks
737
753
  .map((img: any) => {
738
754
  const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
@@ -742,6 +758,7 @@ export class ToolExecutionComponent extends Container {
742
758
  output = output ? `${output}\n${imageIndicators}` : imageIndicators;
743
759
  }
744
760
 
761
+ this.#textOutputCache = { content, showImages: this.#showImages, terminalImageProtocol, output };
745
762
  return output;
746
763
  }
747
764
 
@@ -20,6 +20,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
20
20
  import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
21
21
  import type { ResolveToolDetails } from "../../tools/resolve";
22
22
  import { interruptHint } from "../shared";
23
+ import { buildAbortDisplayMessage } from "../utils/abort-message";
23
24
 
24
25
  type AgentSessionEventKind = AgentSessionEvent["type"];
25
26
 
@@ -419,10 +420,10 @@ export class EventController {
419
420
  // controller ran, so reaching this branch implies the abort was NOT a
420
421
  // silent internal transition.
421
422
  const retryAttempt = this.ctx.session.retryAttempt;
422
- errorMessage =
423
- retryAttempt > 0
424
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
425
- : "Operation aborted";
423
+ errorMessage = buildAbortDisplayMessage({
424
+ errorMessage: this.ctx.streamingMessage.errorMessage,
425
+ retryAttempt,
426
+ });
426
427
  this.ctx.streamingMessage.errorMessage = errorMessage;
427
428
  }
428
429
  if (silentlyAborted || ttsrSilenced) {
@@ -5,6 +5,7 @@ import type { Component, OverlayHandle } from "@gajae-code/tui";
5
5
  import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
7
7
  import { activateModelProfile } from "../../config/model-profile-activation";
8
+ import { recommendModelProfileForProvider } from "../../config/model-profiles";
8
9
  import { settings } from "../../config/settings";
9
10
  import { DebugSelectorComponent } from "../../debug";
10
11
  import { disableProvider, enableProvider } from "../../discovery";
@@ -45,7 +46,7 @@ import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "
45
46
  import { ExtensionDashboard } from "../components/extensions";
46
47
  import { HistorySearchComponent } from "../components/history-search";
47
48
  import { JobsOverlayComponent } from "../components/jobs-overlay";
48
- import { ModelSelectorComponent, type ModelSelectorSelection } from "../components/model-selector";
49
+ import { ModelSelectorComponent } from "../components/model-selector";
49
50
  import { OAuthSelectorComponent } from "../components/oauth-selector";
50
51
  import { PluginSelectorComponent } from "../components/plugin-selector";
51
52
  import {
@@ -537,12 +538,6 @@ export class SelectorController {
537
538
  this.ctx.session.scopedModels,
538
539
  async selection => {
539
540
  try {
540
- if (selection.kind === "preset") {
541
- await this.#applyModelAssignmentPreset(selection);
542
- done();
543
- this.ctx.ui.requestRender();
544
- return;
545
- }
546
541
  if (selection.kind === "profile") {
547
542
  await activateModelProfile(
548
543
  {
@@ -609,45 +604,12 @@ export class SelectorController {
609
604
  done();
610
605
  this.ctx.ui.requestRender();
611
606
  },
612
- options,
607
+ { ...options, sessionId: this.ctx.session.sessionId },
613
608
  );
614
609
  return { component: selector, focus: selector };
615
610
  });
616
611
  }
617
612
 
618
- async #applyModelAssignmentPreset(selection: Extract<ModelSelectorSelection, { kind: "preset" }>): Promise<void> {
619
- const { assignments, model, preset, selector } = selection;
620
- const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
621
- if (!apiKey) {
622
- throw new Error(`No API key for ${model.provider}/${model.id}`);
623
- }
624
-
625
- const defaultThinkingLevel = assignments.default;
626
- await this.ctx.session.setModel(model, "default", {
627
- selector,
628
- thinkingLevel: defaultThinkingLevel,
629
- });
630
- if (defaultThinkingLevel && defaultThinkingLevel !== ThinkingLevel.Inherit) {
631
- this.ctx.session.setThinkingLevel(defaultThinkingLevel);
632
- }
633
-
634
- const overrides = this.ctx.settings.get("task.agentModelOverrides");
635
- const nextOverrides = { ...overrides };
636
- for (const [targetRole, presetThinkingLevel] of Object.entries(assignments) as [
637
- keyof Extract<ModelSelectorSelection, { kind: "preset" }>["assignments"],
638
- ThinkingLevel,
639
- ][]) {
640
- if (!targetRole || targetRole === "default") continue;
641
- nextOverrides[targetRole] =
642
- presetThinkingLevel === ThinkingLevel.Inherit ? selector : `${selector}:${presetThinkingLevel}`;
643
- }
644
- this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
645
- this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
646
- this.ctx.statusLine.invalidate();
647
- this.ctx.updateEditorBorderColor();
648
- this.ctx.showStatus(`${preset.label}: ${selector}`);
649
- }
650
-
651
613
  async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
652
614
  const mgr = new MarketplaceManager({
653
615
  marketplacesRegistryPath: getMarketplacesRegistryPath(),
@@ -1034,6 +996,34 @@ export class SelectorController {
1034
996
  await this.showSessionSelector();
1035
997
  }
1036
998
 
999
+ async #handlePostLoginModelProfileRecommendation(providerId: string): Promise<void> {
1000
+ const recommendedProfile = recommendModelProfileForProvider(
1001
+ providerId,
1002
+ this.ctx.session.modelRegistry.getModelProfiles(),
1003
+ );
1004
+ if (!recommendedProfile) {
1005
+ return;
1006
+ }
1007
+
1008
+ const activeProfile = this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default");
1009
+ if (activeProfile) {
1010
+ this.ctx.showStatus(`Preset ${recommendedProfile.name} is available in /model.`);
1011
+ return;
1012
+ }
1013
+
1014
+ const confirmed = await this.ctx.showHookConfirm(`Apply ${recommendedProfile.name} now?`, "");
1015
+ if (!confirmed) {
1016
+ return;
1017
+ }
1018
+
1019
+ await activateModelProfile({
1020
+ session: this.ctx.session,
1021
+ modelRegistry: this.ctx.session.modelRegistry,
1022
+ settings: this.ctx.settings,
1023
+ profileName: recommendedProfile.name,
1024
+ });
1025
+ }
1026
+
1037
1027
  async #handleOAuthLogin(providerId: string): Promise<void> {
1038
1028
  this.ctx.showStatus(`Logging in to ${providerId}…`);
1039
1029
  const manualInput = this.ctx.oauthManualInput;
@@ -1090,6 +1080,7 @@ export class SelectorController {
1090
1080
  new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
1091
1081
  );
1092
1082
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
1083
+ await this.#handlePostLoginModelProfileRecommendation(providerId);
1093
1084
  this.ctx.ui.requestRender();
1094
1085
  } catch (error: unknown) {
1095
1086
  this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1120,7 +1111,7 @@ export class SelectorController {
1120
1111
  async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
1121
1112
  if (providerId) {
1122
1113
  const oauthProvider = getOAuthProviders().find(provider => provider.id === providerId);
1123
- if (!oauthProvider) {
1114
+ if (!oauthProvider && !this.ctx.session.modelRegistry.getModelProfiles().has(providerId)) {
1124
1115
  this.ctx.showError(`Unknown OAuth provider: ${providerId}`);
1125
1116
  return;
1126
1117
  }
@@ -1057,7 +1057,7 @@ export class InteractiveMode implements InteractiveModeContext {
1057
1057
  return;
1058
1058
  }
1059
1059
  if (event.state?.enabled === true && !this.#goalModePreviousTools) {
1060
- this.#goalModePreviousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1060
+ this.#goalModePreviousTools = this.session.getActiveToolNames();
1061
1061
  }
1062
1062
  this.goalModeEnabled = event.state?.enabled === true;
1063
1063
  this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
@@ -1146,10 +1146,9 @@ export class InteractiveMode implements InteractiveModeContext {
1146
1146
  const restored = await this.session.goalRuntime.onThreadResumed();
1147
1147
  this.goalModeEnabled = restored?.enabled === true;
1148
1148
  this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
1149
- // sdk.ts excludes "goal" from the initial active tool set unconditionally.
1150
- // Re-add it now so the agent can call resume, complete, or drop on this goal.
1149
+ // Keep `goal` armed on resumed threads; it is part of the default active tool set.
1151
1150
  if (restored?.goal) {
1152
- const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1151
+ const previousTools = this.session.getActiveToolNames();
1153
1152
  this.#goalModePreviousTools = previousTools;
1154
1153
  await this.session.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
1155
1154
  }
@@ -1318,7 +1317,7 @@ export class InteractiveMode implements InteractiveModeContext {
1318
1317
  this.showWarning("Exit plan mode first.");
1319
1318
  return;
1320
1319
  }
1321
- const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
1320
+ const previousTools = this.session.getActiveToolNames();
1322
1321
  const goalTools = [...new Set([...previousTools, "goal"])];
1323
1322
  this.#goalModePreviousTools = previousTools;
1324
1323
  this.goalModePaused = false;
@@ -72,7 +72,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
72
72
  // In text mode, output final response
73
73
  if (mode === "text") {
74
74
  const state = session.state;
75
- const lastMessage = state.messages[state.messages.length - 1];
75
+ const lastMessage = state.messages.findLast(message => message.role === "assistant");
76
76
 
77
77
  if (lastMessage?.role === "assistant") {
78
78
  const assistantMsg = lastMessage as AssistantMessage;
@@ -12,6 +12,7 @@ import type { SessionStats } from "../../session/agent-session";
12
12
  import type {
13
13
  RpcCommand,
14
14
  RpcExtensionUIRequest,
15
+ RpcGetStateInclude,
15
16
  RpcHandoffResult,
16
17
  RpcHostToolCallRequest,
17
18
  RpcHostToolCancelRequest,
@@ -442,8 +443,8 @@ export class RpcClient {
442
443
  /**
443
444
  * Get current session state.
444
445
  */
445
- async getState(): Promise<RpcSessionState> {
446
- const response = await this.#send({ type: "get_state" });
446
+ async getState(include?: RpcGetStateInclude[]): Promise<RpcSessionState> {
447
+ const response = await this.#send(include ? { type: "get_state", include } : { type: "get_state" });
447
448
  return this.#getData(response);
448
449
  }
449
450
 
@@ -11,7 +11,7 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
  import * as path from "node:path";
14
- import { $env, readJsonl, Snowflake } from "@gajae-code/utils";
14
+ import { $env, readLines, Snowflake } from "@gajae-code/utils";
15
15
  import type {
16
16
  ExtensionUIContext,
17
17
  ExtensionUIDialogOptions,
@@ -169,6 +169,7 @@ export async function runRpcMode(
169
169
  process.stdout.write(`${JSON.stringify(obj)}\n`);
170
170
  };
171
171
  const emitRpcTitles = shouldEmitRpcTitles();
172
+ const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
172
173
 
173
174
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
174
175
  const hostToolBridge = new RpcHostToolBridge(output);
@@ -229,6 +230,28 @@ export async function runRpcMode(
229
230
 
230
231
  // Shutdown request flag (wrapped in object to allow mutation with const)
231
232
  const shutdownState = { requested: false };
233
+ let shutdownStarted = false;
234
+ async function shutdown(exitCode: number, reason: string): Promise<never> {
235
+ if (shutdownStarted) {
236
+ process.exit(exitCode);
237
+ }
238
+ shutdownStarted = true;
239
+ hostToolBridge.rejectAllPending(`${reason} before host tool execution completed`);
240
+ hostUriBridge.clear(`${reason} before host URI request completed`);
241
+ try {
242
+ await session.sessionManager.ensureOnDisk();
243
+ } catch (err) {
244
+ output(error(undefined, "shutdown", decodeError(err)));
245
+ process.exit(1);
246
+ }
247
+ try {
248
+ await session.dispose();
249
+ } catch (err) {
250
+ output(error(undefined, "shutdown", decodeError(err)));
251
+ process.exit(1);
252
+ }
253
+ process.exit(exitCode);
254
+ }
232
255
 
233
256
  /**
234
257
  * Extension UI context that uses the RPC protocol.
@@ -497,16 +520,24 @@ export async function runRpcMode(
497
520
  */
498
521
  async function checkShutdownRequested(): Promise<void> {
499
522
  if (!shutdownState.requested) return;
523
+ await shutdown(0, "RPC shutdown requested");
524
+ }
500
525
 
501
- if (session.extensionRunner?.hasHandlers("session_shutdown")) {
502
- await session.extensionRunner.emit({ type: "session_shutdown" });
503
- }
526
+ // Listen for JSONL input using Bun's stdin. Parse frame-by-frame so a malformed
527
+ // command reports a parse error without poisoning the whole long-lived RPC session.
528
+ const inputDecoder = new TextDecoder("utf-8", { fatal: false });
529
+ for await (const line of readLines(Bun.stdin.stream())) {
530
+ const text = inputDecoder.decode(line).trim();
531
+ if (!text) continue;
504
532
 
505
- process.exit(0);
506
- }
533
+ let parsed: unknown;
534
+ try {
535
+ parsed = JSON.parse(text);
536
+ } catch (err) {
537
+ output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
538
+ continue;
539
+ }
507
540
 
508
- // Listen for JSON input using Bun's stdin
509
- for await (const parsed of readJsonl(Bun.stdin.stream())) {
510
541
  try {
511
542
  // Handle extension UI responses
512
543
  if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
@@ -540,13 +571,12 @@ export async function runRpcMode(
540
571
 
541
572
  // Check for deferred shutdown request (idle between commands)
542
573
  await checkShutdownRequested();
543
- } catch (e: any) {
544
- output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
574
+ } catch (err) {
575
+ output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
545
576
  }
546
577
  }
547
578
 
548
- // stdin closed — RPC client is gone, exit cleanly
549
- hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
550
- hostUriBridge.clear("RPC client disconnected before host URI request completed");
551
- process.exit(0);
579
+ // stdin closed — RPC client is gone, flush durable state and exit cleanly
580
+ await shutdown(0, "RPC client disconnected");
581
+ throw new Error("RPC shutdown returned unexpectedly");
552
582
  }
@@ -12,6 +12,8 @@ import type { ContextUsage } from "../../extensibility/extensions/types";
12
12
  import type { SessionStats } from "../../session/agent-session";
13
13
  import type { TodoPhase } from "../../tools/todo-write";
14
14
 
15
+ export type RpcGetStateInclude = "tools" | "dumpTools" | "systemPrompt";
16
+
15
17
  // ============================================================================
16
18
  // RPC Commands (stdin)
17
19
  // ============================================================================
@@ -26,7 +28,7 @@ export type RpcCommand =
26
28
  | { id?: string; type: "new_session"; parentSession?: string }
27
29
 
28
30
  // State
29
- | { id?: string; type: "get_state" }
31
+ | { id?: string; type: "get_state"; include?: RpcGetStateInclude[] }
30
32
  | { id?: string; type: "set_todos"; phases: TodoPhase[] }
31
33
  | { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
32
34
  | { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
@@ -99,8 +101,9 @@ export interface RpcSessionState {
99
101
  messageCount: number;
100
102
  queuedMessageCount: number;
101
103
  todoPhases: TodoPhase[];
102
- /** For session dump / export (plain-text parity with /dump). */
104
+ /** Optional static system prompt blocks. Omitted by default; request with get_state include ["systemPrompt"]. */
103
105
  systemPrompt?: string[];
106
+ /** Optional static tool schemas. Omitted by default; request with get_state include ["tools"]. */
104
107
  dumpTools?: Array<{ name: string; description: string; parameters: unknown }>;
105
108
  /** Current context window usage. Null tokens/percent when unknown (e.g. right after compaction). */
106
109
  contextUsage?: ContextUsage;
@@ -183,14 +183,19 @@ export async function dispatchRpcCommand(
183
183
  messageCount: session.messages.length,
184
184
  queuedMessageCount: session.queuedMessageCount,
185
185
  todoPhases: session.getTodoPhases(),
186
- systemPrompt: session.systemPrompt,
187
- dumpTools: session.agent.state.tools.map(tool => ({
186
+ contextUsage: session.getContextUsage(),
187
+ };
188
+ const include = new Set(command.include ?? []);
189
+ if (include.has("systemPrompt")) {
190
+ state.systemPrompt = session.systemPrompt;
191
+ }
192
+ if (include.has("tools") || include.has("dumpTools")) {
193
+ state.dumpTools = session.agent.state.tools.map(tool => ({
188
194
  name: tool.name,
189
195
  description: tool.description,
190
196
  parameters: tool.parameters,
191
- })),
192
- contextUsage: session.getContextUsage(),
193
- };
197
+ }));
198
+ }
194
199
  return rpcSuccess(id, "get_state", state);
195
200
  }
196
201
 
@@ -28,6 +28,15 @@ function stringArray(value: unknown): value is string[] {
28
28
  return Array.isArray(value) && value.every(item => typeof item === "string");
29
29
  }
30
30
 
31
+ const GET_STATE_INCLUDES = new Set(["tools", "dumpTools", "systemPrompt"]);
32
+
33
+ function optionalGetStateInclude(value: unknown): boolean {
34
+ return (
35
+ value === undefined ||
36
+ (Array.isArray(value) && value.every(item => typeof item === "string" && GET_STATE_INCLUDES.has(item)))
37
+ );
38
+ }
39
+
31
40
  function todoPhase(value: unknown): boolean {
32
41
  if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.tasks)) return false;
33
42
  return value.tasks.every(
@@ -96,7 +105,9 @@ export function isRpcCommand(value: unknown): value is RpcCommand {
96
105
  case "follow_up":
97
106
  return stringField(value, "message") && optionalArray(value.images);
98
107
  case "abort":
108
+ return true;
99
109
  case "get_state":
110
+ return optionalGetStateInclude(value.include);
100
111
  case "cycle_model":
101
112
  case "get_available_models":
102
113
  case "cycle_thinking_level":
@@ -264,7 +264,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
264
264
  "icon.context": "◫",
265
265
  "icon.cost": "💲",
266
266
  "icon.time": "⏱",
267
- "icon.pi": "π",
267
+ "icon.pi": "🦞",
268
268
  "icon.agents": "👥",
269
269
  "icon.cache": "💾",
270
270
  "icon.input": "⤵",
@@ -686,7 +686,7 @@ const ASCII_SYMBOLS: SymbolMap = {
686
686
  "icon.context": "ctx:",
687
687
  "icon.cost": "$",
688
688
  "icon.time": "t:",
689
- "icon.pi": "pi",
689
+ "icon.pi": "GJC",
690
690
  "icon.agents": "AG",
691
691
  "icon.cache": "cache",
692
692
  "icon.input": "in:",
@@ -0,0 +1,41 @@
1
+ const STREAM_IDLE_TIMEOUT_PATTERN = /\bstream stalled while waiting for the next event\b/i;
2
+ const GENERIC_ABORT_PATTERN = /^Request was aborted\.?$/i;
3
+ const ABORT_DISPLAY_LABEL_PATTERN = /^(?:Operation aborted|Aborted after \d+ retry attempts?)(?::|$)/;
4
+
5
+ export function buildAbortDisplayMessage({
6
+ errorMessage,
7
+ retryAttempt,
8
+ }: {
9
+ errorMessage?: string;
10
+ retryAttempt: number;
11
+ }): string {
12
+ const existingDisplayMessage = normalizeExistingAbortDisplayMessage(errorMessage);
13
+ if (existingDisplayMessage) return existingDisplayMessage;
14
+
15
+ const baseMessage =
16
+ retryAttempt > 0
17
+ ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
18
+ : "Operation aborted";
19
+ const cause = normalizeAbortCause(errorMessage);
20
+ if (!cause) return baseMessage;
21
+
22
+ return `${baseMessage}: ${cause}${streamIdleTimeoutHint(cause)}`;
23
+ }
24
+
25
+ function normalizeExistingAbortDisplayMessage(errorMessage: string | undefined): string {
26
+ const trimmed = errorMessage?.trim();
27
+ if (!trimmed || !ABORT_DISPLAY_LABEL_PATTERN.test(trimmed)) return "";
28
+ return trimmed;
29
+ }
30
+
31
+ function normalizeAbortCause(errorMessage: string | undefined): string {
32
+ const trimmed = errorMessage?.trim();
33
+ if (!trimmed || GENERIC_ABORT_PATTERN.test(trimmed)) return "";
34
+ return trimmed;
35
+ }
36
+
37
+ function streamIdleTimeoutHint(cause: string): string {
38
+ if (!STREAM_IDLE_TIMEOUT_PATTERN.test(cause)) return "";
39
+ const separator = /[.!?]$/.test(cause) ? " " : ". ";
40
+ return `${separator}Hint: set PI_STREAM_IDLE_TIMEOUT_MS=300000 for slow reasoning/proxy streams, or PI_STREAM_IDLE_TIMEOUT_MS=0 to disable the watchdog.`;
41
+ }
@@ -1,8 +1,12 @@
1
1
  import type { AgentMessage } from "@gajae-code/agent-core";
2
2
  import type { CompactionSettings } from "@gajae-code/agent-core/compaction";
3
- import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "@gajae-code/agent-core/compaction";
3
+ import {
4
+ effectiveReserveTokens,
5
+ estimateMessageTokensHeuristic,
6
+ estimateTextTokensHeuristic,
7
+ resolveThresholdTokens,
8
+ } from "@gajae-code/agent-core/compaction";
4
9
  import type { Model } from "@gajae-code/ai";
5
- import { countTokens } from "@gajae-code/natives";
6
10
  import { formatNumber } from "@gajae-code/utils";
7
11
  import type { Skill } from "../../extensibility/skills";
8
12
  import type { AgentSession } from "../../session/agent-session";
@@ -46,7 +50,7 @@ export function estimateSkillsTokens(skills: readonly Skill[]): number {
46
50
  // concatenated form, so encode each piece separately and sum.
47
51
  fragments.push(skill.name, skill.description);
48
52
  }
49
- return countTokens(fragments);
53
+ return estimateTextTokensHeuristic(fragments);
50
54
  }
51
55
 
52
56
  export function estimateToolSchemaTokens(
@@ -61,7 +65,7 @@ export function estimateToolSchemaTokens(
61
65
  // Schema may contain functions or cycles; ignore.
62
66
  }
63
67
  }
64
- return countTokens(fragments);
68
+ return estimateTextTokensHeuristic(fragments);
65
69
  }
66
70
 
67
71
  /**
@@ -100,8 +104,11 @@ function computeNonMessageBreakdown(session: AgentSession): {
100
104
  const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
101
105
  const systemPromptParts = session.systemPrompt ?? [];
102
106
  const rulesTokens = estimateRulesTokens(systemPromptParts);
103
- const systemContextTokens = countTokens(systemPromptParts.slice(1));
104
- const systemPromptTokens = Math.max(0, countTokens(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens);
107
+ const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
108
+ const systemPromptTokens = Math.max(
109
+ 0,
110
+ estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
111
+ );
105
112
  return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
106
113
  }
107
114
 
@@ -112,7 +119,7 @@ function estimateRulesTokens(systemPromptParts: readonly string[]): number {
112
119
  fragments.push(match[0]);
113
120
  }
114
121
  }
115
- return fragments.length === 0 ? 0 : countTokens(fragments);
122
+ return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
116
123
  }
117
124
 
118
125
  function splitLastUserTurn(messages: readonly AgentMessage[]): {
@@ -130,7 +137,7 @@ function splitLastUserTurn(messages: readonly AgentMessage[]): {
130
137
  let regularMessagesTokens = 0;
131
138
  let lastUserTurnTokens = 0;
132
139
  for (let i = 0; i < messages.length; i++) {
133
- const tokens = estimateTokens(messages[i]);
140
+ const tokens = estimateMessageTokensHeuristic(messages[i]);
134
141
  if (i === lastUserIndex) {
135
142
  lastUserTurnTokens = tokens;
136
143
  } else {