@gajae-code/coding-agent 0.7.10 → 0.8.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 (148) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +2 -2
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/config/keybindings.d.ts +11 -5
  5. package/dist/types/config/settings-schema.d.ts +25 -2
  6. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  7. package/dist/types/discovery/cursor.d.ts +4 -1
  8. package/dist/types/discovery/index.d.ts +0 -1
  9. package/dist/types/discovery/windsurf.d.ts +3 -1
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  12. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  15. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  16. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  17. package/dist/types/modes/components/skill-message.d.ts +1 -0
  18. package/dist/types/modes/components/status-line.d.ts +8 -0
  19. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  20. package/dist/types/modes/components/welcome.d.ts +9 -1
  21. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  22. package/dist/types/modes/controllers/input-controller.d.ts +5 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  24. package/dist/types/modes/interactive-mode.d.ts +2 -0
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  26. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  27. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  28. package/dist/types/modes/types.d.ts +2 -0
  29. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  30. package/dist/types/runtime-mcp/config.d.ts +2 -0
  31. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  32. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  33. package/dist/types/runtime-mcp/types.d.ts +6 -0
  34. package/dist/types/sdk.d.ts +1 -1
  35. package/dist/types/session/agent-session.d.ts +8 -5
  36. package/dist/types/slash-commands/types.d.ts +2 -0
  37. package/dist/types/system-prompt.d.ts +7 -0
  38. package/dist/types/utils/cmux-workspace.d.ts +30 -1
  39. package/dist/types/web/search/index.d.ts +3 -1
  40. package/dist/types/web/search/provider.d.ts +9 -0
  41. package/dist/types/web/search/providers/utils.d.ts +42 -13
  42. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  43. package/package.json +10 -10
  44. package/src/capability/mcp.ts +2 -0
  45. package/src/cli/update-cli.ts +24 -0
  46. package/src/cli/web-search-cli.ts +2 -5
  47. package/src/cli.ts +1 -1
  48. package/src/commands/launch.ts +1 -1
  49. package/src/commands/ralplan.ts +1 -0
  50. package/src/config/keybindings.ts +14 -5
  51. package/src/config/mcp-schema.json +5 -1
  52. package/src/config/model-profiles.ts +16 -0
  53. package/src/config/settings-schema.ts +18 -2
  54. package/src/coordinator-mcp/server.ts +15 -3
  55. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  56. package/src/defaults/gjc-defaults.ts +16 -0
  57. package/src/discovery/builtin.ts +12 -0
  58. package/src/discovery/cursor.ts +5 -84
  59. package/src/discovery/gemini.ts +3 -89
  60. package/src/discovery/index.ts +4 -1
  61. package/src/discovery/mcp-json.ts +11 -0
  62. package/src/discovery/opencode.ts +5 -122
  63. package/src/discovery/windsurf.ts +4 -82
  64. package/src/edit/read-file.ts +9 -1
  65. package/src/extensibility/extensions/runner.ts +2 -1
  66. package/src/gjc-runtime/launch-tmux.ts +164 -42
  67. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  68. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +167 -24
  70. package/src/gjc-runtime/state-runtime.ts +8 -5
  71. package/src/gjc-runtime/state-writer.ts +5 -5
  72. package/src/gjc-runtime/team-runtime.ts +56 -51
  73. package/src/internal-urls/docs-index.generated.ts +8 -7
  74. package/src/main.ts +2 -2
  75. package/src/migrate/executor.ts +23 -6
  76. package/src/modes/components/agent-dashboard.ts +1 -34
  77. package/src/modes/components/custom-editor.ts +26 -0
  78. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  79. package/src/modes/components/model-selector.ts +64 -4
  80. package/src/modes/components/session-selector.ts +19 -9
  81. package/src/modes/components/settings-selector.ts +13 -2
  82. package/src/modes/components/skill-message.ts +18 -10
  83. package/src/modes/components/status-line.ts +215 -86
  84. package/src/modes/components/tree-selector.ts +28 -8
  85. package/src/modes/components/two-column-body.ts +36 -0
  86. package/src/modes/components/welcome.ts +171 -35
  87. package/src/modes/controllers/command-controller.ts +23 -0
  88. package/src/modes/controllers/event-controller.ts +4 -0
  89. package/src/modes/controllers/input-controller.ts +192 -39
  90. package/src/modes/controllers/selector-controller.ts +25 -28
  91. package/src/modes/interactive-mode.ts +36 -30
  92. package/src/modes/prompt-action-autocomplete.ts +23 -1
  93. package/src/modes/rpc/rpc-types.ts +1 -0
  94. package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
  95. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  96. package/src/modes/slash-command-visibility.ts +9 -0
  97. package/src/modes/types.ts +2 -0
  98. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  99. package/src/modes/utils/ui-helpers.ts +76 -3
  100. package/src/notifications/index.ts +20 -1
  101. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  102. package/src/notifications/telegram-daemon.ts +4 -4
  103. package/src/prompts/agents/architect.md +7 -4
  104. package/src/prompts/agents/critic.md +15 -6
  105. package/src/prompts/agents/planner.md +8 -3
  106. package/src/prompts/system/system-prompt.md +15 -1
  107. package/src/prompts/tools/bash.md +1 -1
  108. package/src/runtime-mcp/config-writer.ts +28 -0
  109. package/src/runtime-mcp/config.ts +7 -0
  110. package/src/runtime-mcp/loader.ts +3 -0
  111. package/src/runtime-mcp/manager.ts +3 -0
  112. package/src/runtime-mcp/types.ts +6 -0
  113. package/src/sdk.ts +7 -6
  114. package/src/session/agent-session.ts +81 -18
  115. package/src/session/session-manager.ts +58 -18
  116. package/src/slash-commands/builtin-registry.ts +30 -10
  117. package/src/slash-commands/types.ts +2 -0
  118. package/src/system-prompt.ts +8 -0
  119. package/src/task/executor.ts +5 -0
  120. package/src/tools/ast-grep.ts +10 -7
  121. package/src/tools/bash.ts +39 -14
  122. package/src/tools/index.ts +8 -1
  123. package/src/utils/cmux-workspace.ts +134 -7
  124. package/src/utils/title-generator.ts +1 -1
  125. package/src/web/kagi.ts +1 -1
  126. package/src/web/parallel.ts +2 -2
  127. package/src/web/search/index.ts +118 -45
  128. package/src/web/search/provider.ts +65 -0
  129. package/src/web/search/providers/anthropic.ts +1 -1
  130. package/src/web/search/providers/brave.ts +1 -1
  131. package/src/web/search/providers/codex.ts +1 -1
  132. package/src/web/search/providers/duckduckgo.ts +1 -1
  133. package/src/web/search/providers/exa.ts +1 -1
  134. package/src/web/search/providers/gemini.ts +7 -3
  135. package/src/web/search/providers/jina.ts +1 -1
  136. package/src/web/search/providers/kimi.ts +6 -1
  137. package/src/web/search/providers/openai-compatible.ts +1 -1
  138. package/src/web/search/providers/parallel.ts +1 -1
  139. package/src/web/search/providers/perplexity.ts +2 -2
  140. package/src/web/search/providers/searxng.ts +1 -1
  141. package/src/web/search/providers/synthetic.ts +1 -1
  142. package/src/web/search/providers/tavily.ts +1 -1
  143. package/src/web/search/providers/utils.ts +67 -17
  144. package/src/web/search/providers/xai.ts +1 -1
  145. package/src/web/search/providers/zai.ts +1 -1
  146. package/src/workflow/workflow-intent-diff.ts +199 -0
  147. package/dist/types/discovery/vscode.d.ts +0 -1
  148. package/src/discovery/vscode.ts +0 -105
package/src/sdk.ts CHANGED
@@ -119,6 +119,7 @@ import { AgentOutputManager } from "./task/output-manager";
119
119
  import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
120
120
  import { collectDiscoverableTools, type DiscoverableTool } from "./tool-discovery/tool-index";
121
121
  import {
122
+ applyConfiguredSearchTimeout,
122
123
  BashTool,
123
124
  BUILTIN_TOOLS,
124
125
  computeEssentialBuiltinNames,
@@ -140,7 +141,6 @@ import {
140
141
  setPreferredImageProvider,
141
142
  setPreferredSearchProvider,
142
143
  setSearchFallbackProviders,
143
- setSearchHardTimeoutMs,
144
144
  type Tool,
145
145
  type ToolSession,
146
146
  WebSearchTool,
@@ -255,7 +255,7 @@ export interface CreateAgentSessionOptions {
255
255
  thinkingLevel?: ThinkingLevel;
256
256
  /** Runtime substitution metadata for the initial model_change session event. */
257
257
  modelSubstitution?: { requestedModel: Model; reason: string };
258
- /** Models available for cycling (Ctrl+P in interactive mode) */
258
+ /** Models available for cycling (Alt+N in interactive mode) */
259
259
  scopedModels?: ScopedModelSelection[];
260
260
 
261
261
  /** System prompt blocks. Array replaces default, function receives default blocks and returns final blocks. */
@@ -940,10 +940,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
940
940
  webSearchFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
941
941
  );
942
942
  }
943
- const webSearchTimeout = settings.get("web_search.timeout");
944
- if (typeof webSearchTimeout === "number" && Number.isFinite(webSearchTimeout) && webSearchTimeout > 0) {
945
- setSearchHardTimeoutMs(webSearchTimeout * 1000);
946
- }
943
+ applyConfiguredSearchTimeout(settings);
947
944
 
948
945
  const imageProvider = settings.get("providers.image");
949
946
  if (
@@ -1819,6 +1816,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1819
1816
  intentField,
1820
1817
  mcpDiscoveryMode: false,
1821
1818
  mcpDiscoveryServerSummaries: [],
1819
+ toolDiscoveryActive: effectiveDiscoveryMode === "all",
1820
+ discoverableTools: discoverableBuiltinTools
1821
+ .map(tool => ({ name: tool.name, summary: tool.summary }))
1822
+ .sort((a, b) => a.name.localeCompare(b.name)),
1822
1823
  eagerTasks,
1823
1824
  secretsEnabled,
1824
1825
  workspaceTree: workspaceTreePromise,
@@ -192,7 +192,10 @@ import {
192
192
  modeStatePath as sessionModeStatePath,
193
193
  sessionStateDir,
194
194
  } from "../gjc-runtime/session-layout";
195
- import { persistCoordinatorRuntimeStateFromEvent } from "../gjc-runtime/session-state-sidecar";
195
+ import {
196
+ persistCoordinatorRuntimeStateFromEvent,
197
+ registerCoordinatorRuntimeStateFinalizer,
198
+ } from "../gjc-runtime/session-state-sidecar";
196
199
  import { writeArtifact } from "../gjc-runtime/state-writer";
197
200
  import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
198
201
  import { GoalRuntime } from "../goals/runtime";
@@ -260,6 +263,7 @@ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
260
263
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
261
264
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
262
265
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
266
+ import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
263
267
  import type { AuthStorage } from "./auth-storage";
264
268
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
265
269
  import {
@@ -353,7 +357,7 @@ export interface AgentSessionConfig {
353
357
  agent: Agent;
354
358
  sessionManager: SessionManager;
355
359
  settings: Settings;
356
- /** Models to cycle through with Ctrl+P (from --models flag) */
360
+ /** Models to cycle through with Alt+N (from --models flag) */
357
361
  scopedModels?: ScopedModelSelection[];
358
362
  /** Initial session thinking selector. */
359
363
  thinkingLevel?: ThinkingLevel;
@@ -901,8 +905,9 @@ function extractPermissionLocations(
901
905
  * `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
902
906
  * custom messages queued during streaming) and is matched by the custom-role
903
907
  * `message_start` dequeue branch; user-message pushes leave it undefined and
904
- * rely on the existing text-equality match. */
905
- type QueuedDisplayEntry = { text: string; tag?: string };
908
+ * rely on the existing text-equality match. `sequence` preserves cross-queue
909
+ * insertion order for one-at-a-time dequeue/edit UX. */
910
+ type QueuedDisplayEntry = { text: string; tag?: string; sequence: number };
906
911
 
907
912
  /** A custom message contributed at the before-agent-start point. */
908
913
  export type BeforeAgentStartInternalMessage = Pick<
@@ -956,6 +961,7 @@ export class AgentSession {
956
961
  /** Tracks pending follow-up messages for UI display. Removed when delivered.
957
962
  * See `#steeringMessages` for entry shape. */
958
963
  #followUpMessages: QueuedDisplayEntry[] = [];
964
+ #queuedDisplaySequence = 0;
959
965
  /** Messages queued to be included with the next user prompt as context ("asides"). */
960
966
  #pendingNextTurnMessages: CustomMessage[] = [];
961
967
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
@@ -1007,6 +1013,7 @@ export class AgentSession {
1007
1013
  #evalKernelOwnerId: string;
1008
1014
  /** Idempotent unregister handle for this session's resource-GC registration. */
1009
1015
  #unregisterResourceGc?: () => void;
1016
+ #unregisterRuntimeStateFinalizer?: () => void;
1010
1017
  /**
1011
1018
  * AsyncJobManager owned by this session (top-level only). Subagents leave
1012
1019
  * this undefined and **MUST NOT** dispose the global instance on teardown.
@@ -1232,6 +1239,11 @@ export class AgentSession {
1232
1239
  settings: this.settings,
1233
1240
  });
1234
1241
  }
1242
+ this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
1243
+ sessionId: this.sessionId,
1244
+ cwd: this.sessionManager.getCwd(),
1245
+ sessionFile: this.sessionManager.getSessionFile(),
1246
+ });
1235
1247
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
1236
1248
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
1237
1249
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
@@ -1625,6 +1637,14 @@ export class AgentSession {
1625
1637
  this.#planCompactAbortPending = false;
1626
1638
  }
1627
1639
 
1640
+ #createQueuedDisplayEntry(text: string, tag?: string): QueuedDisplayEntry {
1641
+ const entry: QueuedDisplayEntry = { text, sequence: ++this.#queuedDisplaySequence };
1642
+ if (tag !== undefined) {
1643
+ entry.tag = tag;
1644
+ }
1645
+ return entry;
1646
+ }
1647
+
1628
1648
  /** Register a compact display string for a custom message that the caller is
1629
1649
  * about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
1630
1650
  * Returns a stable tag the caller MUST embed in
@@ -1638,7 +1658,7 @@ export class AgentSession {
1638
1658
  const tag = `gjc-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
1639
1659
  const displayText = text.trim();
1640
1660
  if (!displayText) return tag;
1641
- const entry: QueuedDisplayEntry = { text: displayText, tag };
1661
+ const entry = this.#createQueuedDisplayEntry(displayText, tag);
1642
1662
  if (mode === "steer") {
1643
1663
  this.#steeringMessages.push(entry);
1644
1664
  } else {
@@ -3339,6 +3359,8 @@ export class AgentSession {
3339
3359
  // No-op when this session opened no tabs. Failure is logged, not thrown.
3340
3360
  this.#unregisterResourceGc?.();
3341
3361
  this.#unregisterResourceGc = undefined;
3362
+ this.#unregisterRuntimeStateFinalizer?.();
3363
+ this.#unregisterRuntimeStateFinalizer = undefined;
3342
3364
  await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
3343
3365
  logger.warn("session dispose: releaseTabsForOwner failed", { error }),
3344
3366
  );
@@ -4818,6 +4840,7 @@ export class AgentSession {
4818
4840
 
4819
4841
  // Expand file-based prompt templates if requested
4820
4842
  const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
4843
+ const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
4821
4844
 
4822
4845
  // If streaming, queue via steer() or followUp() based on option
4823
4846
  if (this.isStreaming) {
@@ -4829,9 +4852,16 @@ export class AgentSession {
4829
4852
  } else {
4830
4853
  await this.#queueSteer(expandedText, options?.images);
4831
4854
  }
4855
+ if (workflowIntentDiff) {
4856
+ this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
4857
+ }
4832
4858
  return;
4833
4859
  }
4834
4860
 
4861
+ if (workflowIntentDiff) {
4862
+ this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
4863
+ }
4864
+
4835
4865
  // Skip eager todo prelude when the user has already queued a directive
4836
4866
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
4837
4867
  const eagerTodoPrelude =
@@ -5285,7 +5315,7 @@ export class AgentSession {
5285
5315
  */
5286
5316
  async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
5287
5317
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5288
- this.#steeringMessages.push({ text: displayText });
5318
+ this.#steeringMessages.push(this.#createQueuedDisplayEntry(displayText));
5289
5319
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5290
5320
  if (images && images.length > 0) {
5291
5321
  content.push(...images);
@@ -5316,7 +5346,7 @@ export class AgentSession {
5316
5346
  */
5317
5347
  async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
5318
5348
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5319
- this.#followUpMessages.push({ text: displayText });
5349
+ this.#followUpMessages.push(this.#createQueuedDisplayEntry(displayText));
5320
5350
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5321
5351
  if (images && images.length > 0) {
5322
5352
  content.push(...images);
@@ -5686,24 +5716,27 @@ export class AgentSession {
5686
5716
  }
5687
5717
 
5688
5718
  /**
5689
- * Pop the last queued message (steering first, then follow-up).
5719
+ * Pop the newest queued message across steering and follow-up queues.
5690
5720
  * Used by dequeue keybinding to restore messages to editor one at a time.
5691
5721
  * Returns the popped entry's `.text`; the tag (if any) dies with the
5692
5722
  * record — no orphan state can outlive the queue entry.
5693
5723
  */
5694
5724
  popLastQueuedMessage(): string | undefined {
5695
- // Pop from steering first (LIFO)
5696
- if (this.#steeringMessages.length > 0) {
5725
+ const steeringEntry = this.#steeringMessages.at(-1);
5726
+ const followUpEntry = this.#followUpMessages.at(-1);
5727
+
5728
+ if (steeringEntry && (!followUpEntry || steeringEntry.sequence > followUpEntry.sequence)) {
5697
5729
  const entry = this.#steeringMessages.pop();
5698
5730
  this.agent.popLastSteer();
5699
5731
  return entry?.text;
5700
5732
  }
5701
- // Then from follow-up
5702
- if (this.#followUpMessages.length > 0) {
5733
+
5734
+ if (followUpEntry) {
5703
5735
  const entry = this.#followUpMessages.pop();
5704
5736
  this.agent.popLastFollowUp();
5705
5737
  return entry?.text;
5706
5738
  }
5739
+
5707
5740
  return undefined;
5708
5741
  }
5709
5742
 
@@ -8932,22 +8965,52 @@ export class AgentSession {
8932
8965
  setAutoRetryEnabled(enabled: boolean): void {
8933
8966
  this.settings.set("retry.enabled", enabled);
8934
8967
  }
8968
+ #isInterruptedRetryTail(message: AgentMessage | undefined): boolean {
8969
+ if (!message) return false;
8970
+ return (
8971
+ message.role === "user" ||
8972
+ message.role === "developer" ||
8973
+ message.role === "toolResult" ||
8974
+ message.role === "fileMention" ||
8975
+ message.role === "custom" ||
8976
+ message.role === "hookMessage"
8977
+ );
8978
+ }
8979
+
8980
+ #isUnresolvedToolUseAssistant(message: AssistantMessage): boolean {
8981
+ return message.stopReason === "toolUse" && message.content.some(content => content.type === "toolCall");
8982
+ }
8983
+
8935
8984
  /**
8936
- * Manually retry the last failed assistant turn.
8937
- * Removes the error message from agent state and re-attempts with a fresh retry budget.
8938
- * @returns true if retry was initiated, false if no failed turn to retry or agent is busy
8985
+ * Manually retry the last failed assistant turn, or resume an interrupted tail
8986
+ * left by a non-graceful process exit after the user/custom/tool-result message
8987
+ * was persisted but before the agent emitted a terminal assistant response.
8988
+ * Removes failed/aborted/unresolved tool-use assistant tails before
8989
+ * re-attempting with a fresh retry budget.
8990
+ * @returns true if retry/resume was initiated, false if no retryable tail exists or agent is busy
8939
8991
  */
8940
8992
  async retry(): Promise<boolean> {
8941
8993
  if (this.isStreaming || this.isCompacting || this.isRetrying) return false;
8942
8994
 
8943
8995
  const messages = this.agent.state.messages;
8944
8996
  const lastMsg = messages[messages.length - 1];
8945
- if (lastMsg?.role !== "assistant") return false;
8997
+ if (!lastMsg) return false;
8998
+
8999
+ if (lastMsg.role !== "assistant") {
9000
+ if (!this.#isInterruptedRetryTail(lastMsg)) return false;
9001
+ this.#retryAttempt = 0;
9002
+ this.#scheduleAgentContinue({ delayMs: 1 });
9003
+ return true;
9004
+ }
8946
9005
 
8947
9006
  const assistantMsg = lastMsg as AssistantMessage;
8948
- if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "aborted") return false;
9007
+ const shouldDropAssistant =
9008
+ assistantMsg.stopReason === "error" ||
9009
+ assistantMsg.stopReason === "aborted" ||
9010
+ this.#isUnresolvedToolUseAssistant(assistantMsg);
9011
+ if (!shouldDropAssistant) return false;
8949
9012
 
8950
- // Remove the failed/aborted assistant message (same as auto-retry does before re-attempting)
9013
+ // Remove the failed/aborted/incomplete assistant message before re-attempting.
8951
9014
  this.agent.replaceMessages(messages.slice(0, -1));
8952
9015
 
8953
9016
  // Reset retry budget for a fresh attempt
@@ -1461,9 +1461,25 @@ function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
1461
1461
  function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
1462
1462
  entries: T[],
1463
1463
  stores: ResidentBlobStores,
1464
+ missingPolicy: ResidentBlobMissingPolicy = "throw",
1464
1465
  ): T[] {
1465
1466
  const cache = new Map<string, string>();
1466
- return entries.map(entry => materializeResidentEntrySync(entry, stores, cache));
1467
+ return entries.map(entry => materializeResidentEntrySync(entry, stores, cache, missingPolicy));
1468
+ }
1469
+
1470
+ function materializeResidentEntryForReadSync<T extends FileEntry | SessionEntry>(
1471
+ entry: T,
1472
+ stores: ResidentBlobStores,
1473
+ cache: Map<string, string>,
1474
+ ): T {
1475
+ return materializeResidentEntrySync(entry, stores, cache, "placeholder");
1476
+ }
1477
+
1478
+ function materializeResidentEntriesForReadSync<T extends FileEntry | SessionEntry>(
1479
+ entries: T[],
1480
+ stores: ResidentBlobStores,
1481
+ ): T[] {
1482
+ return materializeResidentEntriesSync(entries, stores, "placeholder");
1467
1483
  }
1468
1484
 
1469
1485
  function materializeResidentEntryForPersistenceSync<T extends FileEntry | SessionEntry>(
@@ -1520,12 +1536,24 @@ function cloneSessionEntry(entry: SessionEntry): SessionEntry {
1520
1536
  function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: ResidentBlobStores): SessionEntry {
1521
1537
  if (entry.type === "compaction") {
1522
1538
  const cache = new Map<string, string>();
1523
- const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache);
1524
- const shortSummary = materializeResidentValueSync(entry.shortSummary, stores, "shortSummary", cache);
1539
+ const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache, "placeholder");
1540
+ const shortSummary = materializeResidentValueSync(
1541
+ entry.shortSummary,
1542
+ stores,
1543
+ "shortSummary",
1544
+ cache,
1545
+ "placeholder",
1546
+ );
1525
1547
  const remote = entry.preserveData?.openaiRemoteCompaction;
1526
1548
  const remoteRecord = isRecord(remote) ? remote : undefined;
1527
1549
  const replacementHistory = remoteRecord
1528
- ? materializeResidentValueSync(remoteRecord.replacementHistory, stores, "replacementHistory", cache)
1550
+ ? materializeResidentValueSync(
1551
+ remoteRecord.replacementHistory,
1552
+ stores,
1553
+ "replacementHistory",
1554
+ cache,
1555
+ "placeholder",
1556
+ )
1529
1557
  : undefined;
1530
1558
  const preserveData =
1531
1559
  remoteRecord && replacementHistory !== undefined && replacementHistory !== remoteRecord.replacementHistory
@@ -1545,7 +1573,13 @@ function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: Reside
1545
1573
  };
1546
1574
  }
1547
1575
  if (entry.type === "branch_summary") {
1548
- const summary = materializeResidentValueSync(entry.summary, stores, "summary", new Map<string, string>());
1576
+ const summary = materializeResidentValueSync(
1577
+ entry.summary,
1578
+ stores,
1579
+ "summary",
1580
+ new Map<string, string>(),
1581
+ "placeholder",
1582
+ );
1549
1583
  return typeof summary === "string" ? { ...entry, summary } : { ...entry };
1550
1584
  }
1551
1585
  return cloneSessionEntry(entry);
@@ -2751,7 +2785,10 @@ export class SessionManager {
2751
2785
  }
2752
2786
 
2753
2787
  captureState(): SessionManagerStateSnapshot {
2754
- const materializedFileEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2788
+ const materializedFileEntries = materializeResidentEntriesForReadSync(
2789
+ this.#fileEntries,
2790
+ this.#residentBlobStores(),
2791
+ );
2755
2792
  return {
2756
2793
  sessionId: this.#sessionId,
2757
2794
  sessionName: this.#sessionName,
@@ -2875,7 +2912,7 @@ export class SessionManager {
2875
2912
 
2876
2913
  const oldSessionFile = this.#sessionFile;
2877
2914
  const oldSessionId = this.#sessionId;
2878
- const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2915
+ const materializedEntries = materializeResidentEntriesForReadSync(this.#fileEntries, this.#residentBlobStores());
2879
2916
 
2880
2917
  // Close the current writer
2881
2918
  await this.#closePersistWriter();
@@ -2947,7 +2984,10 @@ export class SessionManager {
2947
2984
  hadSessionFile = this.storage.existsSync(oldSessionFile);
2948
2985
  let movedSessionFile = false;
2949
2986
  let movedArtifactDir = false;
2950
- const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2987
+ const materializedEntries = materializeResidentEntriesForReadSync(
2988
+ this.#fileEntries,
2989
+ this.#residentBlobStores(),
2990
+ );
2951
2991
  const restoreResidentStateAfterFailure = (): void => {
2952
2992
  this.#fileEntries = materializedEntries;
2953
2993
  this.#resetResidentTextBlobStore();
@@ -4006,7 +4046,7 @@ export class SessionManager {
4006
4046
  getLeafEntry(): SessionEntry | undefined {
4007
4047
  if (!this.#leafId) return undefined;
4008
4048
  const entry = this.#byId.get(this.#leafId);
4009
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
4049
+ return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
4010
4050
  }
4011
4051
 
4012
4052
  /**
@@ -4176,7 +4216,7 @@ export class SessionManager {
4176
4216
  const entry = this.#byId.get(id);
4177
4217
  return entry
4178
4218
  ? rehydrateColdSpillEntry(
4179
- materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()),
4219
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
4180
4220
  this.#blobStore,
4181
4221
  this.#residentBlobStoresForColdRehydrate(),
4182
4222
  )
@@ -4193,7 +4233,7 @@ export class SessionManager {
4193
4233
  visited.add(current.id);
4194
4234
  path.push(
4195
4235
  rehydrateColdSpillEntry(
4196
- materializeResidentEntrySync(current, this.#residentBlobStores(), cache),
4236
+ materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
4197
4237
  this.#blobStore,
4198
4238
  this.#residentBlobStoresForColdRehydrate(),
4199
4239
  ),
@@ -4233,7 +4273,7 @@ export class SessionManager {
4233
4273
  .filter((entry): entry is SessionEntry => entry.type !== "session")
4234
4274
  .map(entry =>
4235
4275
  rehydrateColdSpillEntry(
4236
- materializeResidentEntrySync(entry, this.#residentBlobStores(), cache),
4276
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
4237
4277
  this.#blobStore,
4238
4278
  this.#residentBlobStoresForColdRehydrate(),
4239
4279
  ),
@@ -4244,7 +4284,7 @@ export class SessionManager {
4244
4284
  this.#publicMaterializerCallCount++;
4245
4285
  this.#getEntryMaterializerCallCount++;
4246
4286
  const entry = this.#byId.get(id);
4247
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
4287
+ return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
4248
4288
  }
4249
4289
 
4250
4290
  /**
@@ -4255,7 +4295,7 @@ export class SessionManager {
4255
4295
  const children: SessionEntry[] = [];
4256
4296
  for (const entry of this.#byId.values()) {
4257
4297
  if (entry.parentId === parentId) {
4258
- children.push(materializeResidentEntrySync(entry, this.#residentBlobStores(), cache));
4298
+ children.push(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache));
4259
4299
  }
4260
4300
  }
4261
4301
  return children;
@@ -4310,7 +4350,7 @@ export class SessionManager {
4310
4350
  while (current) {
4311
4351
  if (visited.has(current.id)) break;
4312
4352
  visited.add(current.id);
4313
- path.push(materializeResidentEntrySync(current, this.#residentBlobStores(), cache));
4353
+ path.push(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache));
4314
4354
  current = current.parentId ? this.#byId.get(current.parentId) : undefined;
4315
4355
  }
4316
4356
  path.reverse();
@@ -4375,7 +4415,7 @@ export class SessionManager {
4375
4415
  }
4376
4416
  if (entry.type !== "message" && entry.type !== "custom_message")
4377
4417
  return materializeProviderVisibleEntrySync(entry, this.#residentBlobStores());
4378
- const materialized = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
4418
+ const materialized = materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map());
4379
4419
  const rehydrated = rehydrateColdSpillEntry(
4380
4420
  materialized,
4381
4421
  this.#blobStore,
@@ -4434,7 +4474,7 @@ export class SessionManager {
4434
4474
  const resolvedTextBlobCache = new Map<string, string>();
4435
4475
  const materializedEntries = this.#fileEntries
4436
4476
  .filter((e): e is SessionEntry => e.type !== "session")
4437
- .map(entry => materializeResidentEntrySync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
4477
+ .map(entry => materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
4438
4478
  this.#materializedEntriesCache = materializedEntries;
4439
4479
  this.#materializedEntriesRevision = this.#entryRevision;
4440
4480
  return materializedEntries;
@@ -4591,7 +4631,7 @@ export class SessionManager {
4591
4631
 
4592
4632
  // Filter out LabelEntry from path - we'll recreate them from the resolved map
4593
4633
  const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
4594
- const materializedPathWithoutLabels = materializeResidentEntriesSync(
4634
+ const materializedPathWithoutLabels = materializeResidentEntriesForReadSync(
4595
4635
  pathWithoutLabels,
4596
4636
  this.#residentBlobStores(),
4597
4637
  );
@@ -322,7 +322,8 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
322
322
  const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
323
323
  {
324
324
  name: "settings",
325
- description: "Open settings menu",
325
+ priority: 40,
326
+ description: "Open settings and preferences",
326
327
  handleTui: (_command, runtime) => {
327
328
  runtime.ctx.showSettingsSelector();
328
329
  runtime.ctx.editor.setText("");
@@ -338,7 +339,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
338
339
  },
339
340
  {
340
341
  name: "goal",
341
- description: "Toggle goal mode (persistent autonomous objective for this session)",
342
+ priority: 84,
343
+ description: "Plan and track an autonomous goal",
342
344
  subcommands: [
343
345
  { name: "set", description: "Set or replace the goal", usage: "<objective>" },
344
346
  { name: "show", description: "Show current goal details" },
@@ -541,7 +543,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
541
543
  },
542
544
  {
543
545
  name: "export",
544
- description: "Export session to HTML file",
546
+ priority: 50,
547
+ description: "Export this session to an HTML file",
545
548
  inlineHint: "[path]",
546
549
  allowArgs: true,
547
550
  handle: async (command, runtime) => {
@@ -568,7 +571,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
568
571
  },
569
572
  {
570
573
  name: "copy",
571
- description: "Copy last response as markdown",
574
+ priority: 55,
575
+ description: "Copy the last response for review or sharing",
572
576
  // Public `/copy` is strict zero-argument, but `allowArgs` lets the
573
577
  // TUI dispatcher route `/copy <arg>` here so it can be rejected locally
574
578
  // instead of falling through as a model prompt.
@@ -585,7 +589,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
585
589
  },
586
590
  {
587
591
  name: "dump",
588
- description: "Copy session transcript to clipboard",
592
+ priority: 54,
593
+ description: "Dump the full transcript for review or sharing",
589
594
  acpDescription: "Return full transcript as plain text",
590
595
  handle: async (_command, runtime) => {
591
596
  const text = runtime.session.formatSessionAsText();
@@ -599,11 +604,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
599
604
  },
600
605
  {
601
606
  name: "session",
602
- description: "Session management commands",
607
+ priority: 88,
608
+ description: "Show current session info or delete current session",
603
609
  acpDescription: "Show session information",
604
610
  acpInputHint: "info|delete",
605
611
  subcommands: [
606
- { name: "info", description: "Show session info and stats" },
612
+ { name: "info", description: "Show current session id, title, and workspace" },
607
613
  { name: "delete", description: "Delete current session and return to selector" },
608
614
  ],
609
615
  allowArgs: true,
@@ -713,6 +719,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
713
719
  runtime.ctx.editor.setText("");
714
720
  },
715
721
  },
722
+ {
723
+ name: "help",
724
+ priority: 100,
725
+ description: "Learn commands and beginner workflows",
726
+ handleTui: (_command, runtime) => {
727
+ runtime.ctx.handleHelpCommand();
728
+ runtime.ctx.editor.setText("");
729
+ },
730
+ },
716
731
  {
717
732
  name: "hotkeys",
718
733
  description: "Show all keyboard shortcuts",
@@ -953,6 +968,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
953
968
  },
954
969
  {
955
970
  name: "new",
971
+ priority: 96,
956
972
  description: "Start a new session",
957
973
  handleTui: async (_command, runtime) => {
958
974
  runtime.ctx.editor.setText("");
@@ -969,7 +985,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
969
985
  },
970
986
  {
971
987
  name: "compact",
972
- description: "Manually compact the session context",
988
+ priority: 72,
989
+ description: "Compact context and continue this session",
973
990
  acpDescription: "Compact the conversation",
974
991
  inlineHint: "[focus instructions]",
975
992
  allowArgs: true,
@@ -1026,7 +1043,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1026
1043
  },
1027
1044
  {
1028
1045
  name: "resume",
1029
- description: "Resume a different session",
1046
+ priority: 92,
1047
+ description: "Resume a previous session",
1030
1048
  handleTui: (_command, runtime) => {
1031
1049
  runtime.ctx.showSessionSelector();
1032
1050
  runtime.ctx.editor.setText("");
@@ -1045,7 +1063,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1045
1063
  },
1046
1064
  {
1047
1065
  name: "retry",
1048
- description: "Retry the last failed agent turn",
1066
+ priority: 70,
1067
+ description: "Retry or continue the last interrupted turn",
1049
1068
  handleTui: async (_command, runtime) => {
1050
1069
  const didRetry = await runtime.ctx.session.retry();
1051
1070
  if (!didRetry) {
@@ -1242,6 +1261,7 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = AC
1242
1261
  description: command.description,
1243
1262
  subcommands: command.subcommands,
1244
1263
  inlineHint: command.inlineHint,
1264
+ priority: command.priority,
1245
1265
  }),
1246
1266
  );
1247
1267
 
@@ -19,6 +19,8 @@ export interface BuiltinSlashCommand {
19
19
  subcommands?: SubcommandDef[];
20
20
  /** Static inline hint when command takes a simple argument (no subcommands). */
21
21
  inlineHint?: string;
22
+ /** Higher values sort earlier in TUI slash-command autocomplete. */
23
+ priority?: number;
22
24
  }
23
25
 
24
26
  /** Parsed slash-command text after stripping the leading "/". */
@@ -356,6 +356,10 @@ export interface BuildSystemPromptOptions {
356
356
  mcpDiscoveryMode?: boolean;
357
357
  /** Discoverable MCP server summaries to advertise when discovery mode is active. */
358
358
  mcpDiscoveryServerSummaries?: string[];
359
+ /** Whether built-in tool discovery ("all" mode) is active; enables the tool-discovery prompt block. */
360
+ toolDiscoveryActive?: boolean;
361
+ /** Hidden discoverable tools to advertise (name + one-line summary) when tool discovery is active. */
362
+ discoverableTools?: Array<{ name: string; summary: string }>;
359
363
  /** Encourage the agent to delegate via tasks unless changes are trivial. */
360
364
  eagerTasks?: boolean;
361
365
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
@@ -394,6 +398,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
394
398
  intentField,
395
399
  mcpDiscoveryMode = false,
396
400
  mcpDiscoveryServerSummaries = [],
401
+ toolDiscoveryActive = false,
402
+ discoverableTools = [],
397
403
  eagerTasks = false,
398
404
  secretsEnabled = false,
399
405
  workspaceTree: providedWorkspaceTree,
@@ -572,6 +578,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
572
578
  mcpDiscoveryMode,
573
579
  hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
574
580
  mcpDiscoveryServerSummaries,
581
+ toolDiscoveryActive,
582
+ discoverableTools,
575
583
  eagerTasks,
576
584
  secretsEnabled,
577
585
  };
@@ -1535,6 +1535,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1535
1535
  );
1536
1536
  await awaitAbortable(session.waitForIdle());
1537
1537
  } catch (err) {
1538
+ // An abort (parent cancel, session shutdown, timeout) rejects
1539
+ // awaitAbortable with ToolAbortError — a cancellation, not a prompt
1540
+ // failure. Mirror the outer catch's `!abortSignal.aborted` guard so
1541
+ // expected cancellations aren't logged at error level.
1542
+ if (abortSignal.aborted || err instanceof ToolAbortError) break;
1538
1543
  logger.error("Subagent prompt failed", {
1539
1544
  error: err instanceof Error ? err.message : String(err),
1540
1545
  });
@@ -223,13 +223,16 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
223
223
  const modelOut: string[] = [];
224
224
  const displayOut: string[] = [];
225
225
  const fileMatches = matchesByFile.get(relativePath) ?? [];
226
- const lineNumberWidth = fileMatches.reduce((width, match) => {
227
- const lineCount = match.text.split("\n").length;
228
- const endLine = match.startLine + lineCount - 1;
229
- return Math.max(width, String(match.startLine).length, String(endLine).length);
230
- }, 0);
231
- for (const match of fileMatches) {
232
- const matchLines = match.text.split("\n");
226
+ const preparedMatches = fileMatches.map(match => {
227
+ const lines = match.text.split("\n");
228
+ const endLine = match.startLine + lines.length - 1;
229
+ return { match, lines, endLine };
230
+ });
231
+ const lineNumberWidth = preparedMatches.reduce(
232
+ (width, entry) => Math.max(width, String(entry.match.startLine).length, String(entry.endLine).length),
233
+ 0,
234
+ );
235
+ for (const { match, lines: matchLines } of preparedMatches) {
233
236
  for (let index = 0; index < matchLines.length; index++) {
234
237
  const lineNumber = match.startLine + index;
235
238
  const isMatch = index === 0;