@gajae-code/coding-agent 0.12.0 → 0.12.2

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 (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -43,7 +43,13 @@ import {
43
43
  setTheme,
44
44
  theme,
45
45
  } from "../../modes/theme/theme";
46
- import type { InteractiveModeContext, OAuthSelectorOptions } from "../../modes/types";
46
+ import {
47
+ clearInteractiveActivityLoaders,
48
+ type InteractiveModeContext,
49
+ type OAuthSelectorOptions,
50
+ stopInteractiveActivityIndicator,
51
+ suspendInteractiveActivityIndicator,
52
+ } from "../../modes/types";
47
53
  import { getNotificationConfig, isTelegramConfigured, maskToken } from "../../sdk/bus/config";
48
54
  import {
49
55
  clearTelegramActivationMarker,
@@ -130,6 +136,7 @@ import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "
130
136
  import { ExtensionDashboard } from "../components/extensions";
131
137
  import type { PetMode } from "../components/gajae-pet-widget";
132
138
  import { HistorySearchComponent } from "../components/history-search";
139
+ import { HookSelectorComponent } from "../components/hook-selector";
133
140
  import { JobsOverlayComponent } from "../components/jobs-overlay";
134
141
  import { ModelSelectorComponent } from "../components/model-selector";
135
142
  import type {
@@ -166,7 +173,7 @@ import type { JobsObserver } from "../jobs-observer";
166
173
  import type { SessionObserverRegistry } from "../session-observer-registry";
167
174
  import type { TasksAggregator } from "../tasks-aggregator";
168
175
  import type { TranscriptItemRegistry } from "../transcript-item-registry";
169
- import { acquireResumeProgressLease } from "../utils/ui-helpers";
176
+ import { acquireResumeProgressLease, type ResumeProgressLease } from "../utils/ui-helpers";
170
177
 
171
178
  const CALLBACK_SERVER_PROVIDERS = new Set<string>([
172
179
  "anthropic",
@@ -196,7 +203,6 @@ export function buildStatusLineSettings(settingsInstance: Settings): StatusLineS
196
203
  rightSegments: settingsInstance.get("statusLine.rightSegments"),
197
204
  separator: settingsInstance.get("statusLine.separator"),
198
205
  showHookStatus: settingsInstance.get("statusLine.showHookStatus"),
199
- showActionHints: settingsInstance.get("statusLine.showActionHints"),
200
206
  sessionAccent: settingsInstance.get("statusLine.sessionAccent"),
201
207
  maxRows: settingsInstance.get("statusLine.maxRows"),
202
208
  segmentOptions: settingsInstance.get("statusLine.segmentOptions"),
@@ -351,6 +357,7 @@ export function createNotificationsEditorOperations(
351
357
  richDraftEnabled: config.richDraft.enabled,
352
358
  toolActivityEnabled: config.toolActivity.enabled,
353
359
  streamingEnabled: config.streaming.enabled,
360
+ sound: config.sound,
354
361
  },
355
362
  };
356
363
  },
@@ -724,6 +731,7 @@ export function createNotificationsEditorOperations(
724
731
  { path: "notifications.redact", op: "set", value: preferences.redact },
725
732
  { path: "notifications.verbosity", op: "set", value: preferences.verbosity },
726
733
  { path: "notifications.sessionScope", op: "set", value: preferences.sessionScope },
734
+ { path: "notifications.telegram.sound", op: "set", value: preferences.sound },
727
735
  { path: "notifications.telegram.rich.enabled", op: "set", value: preferences.richEnabled },
728
736
  { path: "notifications.telegram.richDraft.enabled", op: "set", value: preferences.richDraftEnabled },
729
737
  { path: "notifications.telegram.streaming.enabled", op: "set", value: preferences.streamingEnabled },
@@ -818,7 +826,9 @@ export class SelectorController {
818
826
  * @param create Factory that receives a `done` callback and returns the component and focus target
819
827
  */
820
828
  showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
829
+ if (this.ctx.isStopped?.()) return;
821
830
  const done = () => {
831
+ if (this.ctx.isStopped?.()) return;
822
832
  // Prefer the pet-aware composer restore (InteractiveMode.restoreComposer); fall back
823
833
  // to a plain editor swap for contexts that predate it (e.g. lightweight test doubles).
824
834
  if (typeof this.ctx.restoreComposer === "function") {
@@ -1280,6 +1290,8 @@ export class SelectorController {
1280
1290
  availableThemes,
1281
1291
  availableModelProfiles: [...this.ctx.session.modelRegistry.getModelProfiles().keys()],
1282
1292
  cwd: getProjectDir(),
1293
+ gjcRuntimeSnapshot: this.ctx.session.gjcRuntimeSnapshot,
1294
+ gjcActivationGeneration: this.ctx.session.gjcActivationGeneration,
1283
1295
  },
1284
1296
  {
1285
1297
  onChange: (id, value) => this.handleSettingChange(id, value),
@@ -1561,7 +1573,8 @@ export class SelectorController {
1561
1573
  this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
1562
1574
  break;
1563
1575
  case "theme": {
1564
- setTheme(value as string, true).then(result => {
1576
+ setTheme(value as string, true, { shouldApply: () => !this.ctx.isStopped?.() }).then(result => {
1577
+ if (this.ctx.isStopped?.()) return;
1565
1578
  this.ctx.statusLine.invalidate();
1566
1579
  this.ctx.updateEditorTopBorder();
1567
1580
  this.ctx.ui.invalidate();
@@ -1621,13 +1634,16 @@ export class SelectorController {
1621
1634
  this.ctx.session.agent.repetitionPenalty = repetitionPenalty >= 0 ? repetitionPenalty : undefined;
1622
1635
  break;
1623
1636
  }
1637
+ case "statusLine.showActionHints": {
1638
+ this.ctx.updateEditorChrome();
1639
+ break;
1640
+ }
1624
1641
  case "statusLinePreset":
1625
1642
  case "statusLine.preset":
1626
1643
  case "statusLineSeparator":
1627
1644
  case "statusLine.separator":
1628
1645
  case "statusLineShowHooks":
1629
1646
  case "statusLine.showHookStatus":
1630
- case "statusLine.showActionHints":
1631
1647
  case "statusLine.sessionAccent":
1632
1648
  case "statusLine.maxRows":
1633
1649
  case "statusLine.leftSegments":
@@ -2133,12 +2149,14 @@ export class SelectorController {
2133
2149
 
2134
2150
  // Set up escape handler and loader if summarizing
2135
2151
  let summaryLoader: Loader | undefined;
2152
+ let releaseActivityIndicator: (() => void) | undefined;
2136
2153
  const originalOnEscape = this.ctx.editor.onEscape;
2137
2154
 
2138
2155
  if (wantsSummary) {
2139
2156
  this.ctx.editor.onEscape = () => {
2140
2157
  this.ctx.session.abortBranchSummary();
2141
2158
  };
2159
+ releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
2142
2160
  this.ctx.chatContainer.addChild(new Spacer(1));
2143
2161
  summaryLoader = new Loader(
2144
2162
  this.ctx.ui,
@@ -2156,6 +2174,7 @@ export class SelectorController {
2156
2174
  summarize: wantsSummary,
2157
2175
  customInstructions,
2158
2176
  });
2177
+ if (this.ctx.isStopped?.()) return;
2159
2178
 
2160
2179
  if (result.aborted) {
2161
2180
  // Summarization aborted - re-show tree selector
@@ -2171,18 +2190,21 @@ export class SelectorController {
2171
2190
  // Update UI — pass the context built by navigateTree to skip a second O(N) walk.
2172
2191
  this.ctx.rebuildInitialMessages("reconcile-same-transcript", result.sessionContext);
2173
2192
  await this.ctx.reloadTodos();
2193
+ if (this.ctx.isStopped?.()) return;
2174
2194
  if (result.editorText && !this.ctx.editor.getText().trim()) {
2175
2195
  this.ctx.editor.setText(result.editorText);
2176
2196
  }
2177
2197
  this.ctx.showStatus("Navigated to selected point");
2178
2198
  } catch (error) {
2199
+ if (this.ctx.isStopped?.()) return;
2179
2200
  this.ctx.showError(error instanceof Error ? error.message : String(error));
2180
2201
  } finally {
2181
2202
  if (summaryLoader) {
2182
2203
  summaryLoader.stop();
2183
- this.ctx.statusContainer.clear();
2204
+ if (!this.ctx.isStopped?.()) this.ctx.statusContainer.clear();
2184
2205
  }
2185
- this.ctx.editor.onEscape = originalOnEscape;
2206
+ if (!this.ctx.isStopped?.()) this.ctx.editor.onEscape = originalOnEscape;
2207
+ releaseActivityIndicator?.();
2186
2208
  }
2187
2209
  },
2188
2210
  () => {
@@ -2201,6 +2223,7 @@ export class SelectorController {
2201
2223
 
2202
2224
  async showSessionSelector(): Promise<void> {
2203
2225
  const sessions = await this.ctx.sessionManager.listForResumePickerReadOnly();
2226
+ if (this.ctx.isStopped?.()) return;
2204
2227
  this.showSelector(done => {
2205
2228
  const selector = new SessionSelectorComponent(
2206
2229
  sessions,
@@ -2235,12 +2258,9 @@ export class SelectorController {
2235
2258
  });
2236
2259
  }
2237
2260
 
2238
- #clearTransientSessionUi(): void {
2239
- if (this.ctx.loadingAnimation) {
2240
- this.ctx.loadingAnimation.stop();
2241
- this.ctx.loadingAnimation = undefined;
2242
- }
2243
- this.ctx.statusContainer.clear();
2261
+ #clearTransientSessionUi(options?: { restoreBackground?: boolean; clearSpecializedLoaders?: boolean }): void {
2262
+ if (options?.clearSpecializedLoaders) clearInteractiveActivityLoaders(this.ctx);
2263
+ stopInteractiveActivityIndicator(this.ctx, { restoreBackground: options?.restoreBackground });
2244
2264
  this.ctx.pendingMessagesContainer.clear();
2245
2265
  this.ctx.compactionQueuedMessages = [];
2246
2266
  this.ctx.streamingComponent = undefined;
@@ -2273,6 +2293,7 @@ export class SelectorController {
2273
2293
  }
2274
2294
 
2275
2295
  const detached = await this.ctx.session.newSession();
2296
+ if (this.ctx.isStopped?.()) return false;
2276
2297
  if (!detached) {
2277
2298
  return false;
2278
2299
  }
@@ -2280,28 +2301,50 @@ export class SelectorController {
2280
2301
 
2281
2302
  this.#refreshSessionTerminalTitle();
2282
2303
 
2283
- this.#clearTransientSessionUi();
2304
+ this.#clearTransientSessionUi({ clearSpecializedLoaders: true });
2284
2305
  this.ctx.statusLine.invalidate();
2285
2306
  this.ctx.statusLine.setSessionStartTime(Date.now());
2286
2307
  this.ctx.updateEditorTopBorder();
2287
2308
  this.ctx.updateEditorBorderColor();
2288
2309
  this.ctx.rebuildInitialMessages("replace-identity");
2289
2310
  await this.ctx.reloadTodos();
2311
+ if (this.ctx.isStopped?.()) return false;
2290
2312
  this.ctx.ui.requestRender();
2291
2313
  return true;
2292
2314
  }
2293
2315
 
2316
+ /**
2317
+ * Guards `handleResumeSession` against overlapping resumes.
2318
+ *
2319
+ * `acquireResumeProgressLease` only owns the spinner, so it cannot serialize
2320
+ * two resumes, and the session picker dispatches resume through a
2321
+ * void-returning `onSelect` callback — a second selection would otherwise
2322
+ * start a concurrent transition whose rejection nobody awaits.
2323
+ */
2324
+ #resumeInFlight = false;
2325
+
2294
2326
  async handleResumeSession(sessionPath: string): Promise<void> {
2295
- const previousSessionId = this.ctx.sessionManager.getSessionId();
2296
- this.#clearTransientSessionUi();
2297
- const progressLease = acquireResumeProgressLease(this.ctx);
2327
+ if (this.#resumeInFlight) {
2328
+ this.ctx.showStatus("Resume already in progress");
2329
+ return;
2330
+ }
2331
+ this.#resumeInFlight = true;
2332
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
2333
+ // Everything below runs inside the try so a synchronous failure (a throwing
2334
+ // session-manager accessor, an unavailable status rail) can never strand the
2335
+ // guard and silently disable every later resume.
2336
+ let progressLease: ResumeProgressLease | undefined;
2298
2337
  try {
2338
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
2339
+ progressLease = acquireResumeProgressLease(this.ctx);
2299
2340
  await progressLease.committed;
2341
+ if (this.ctx.isStopped?.()) return;
2300
2342
  const migrationPolicy =
2301
2343
  this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
2302
2344
  let writableSessionPath = sessionPath;
2303
2345
  if (this.ctx.sessionManager.isManagedDestination()) {
2304
2346
  const inspection = await SessionManager.inspectSessionTailReadOnly(sessionPath);
2347
+ if (this.ctx.isStopped?.()) return;
2305
2348
  if (inspection.kind === "error")
2306
2349
  throw new Error(`Could not inspect selected session: ${inspection.reason}`);
2307
2350
  writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForStrictAdoption(
@@ -2309,14 +2352,66 @@ export class SelectorController {
2309
2352
  migrationPolicy,
2310
2353
  inspection.identity,
2311
2354
  );
2355
+ if (this.ctx.isStopped?.()) return;
2312
2356
  }
2313
2357
  // Switch session via AgentSession (emits hook and tool session events)
2314
- if (
2315
- !(await this.ctx.session.switchSession(writableSessionPath, {
2358
+ let switched: boolean;
2359
+ let transitionMutationStarted = false;
2360
+ try {
2361
+ switched = await this.ctx.session.switchSession(writableSessionPath, {
2316
2362
  transition: { origin: INTERACTIVE_SELECTOR_RESUME_ORIGIN },
2317
- }))
2318
- )
2363
+ onTransitionMutationStarted: () => {
2364
+ transitionMutationStarted = true;
2365
+ },
2366
+ });
2367
+ if (this.ctx.isStopped?.()) return;
2368
+ } catch (error) {
2369
+ if (this.ctx.isStopped?.()) return;
2370
+ // `switchSession` opens with `#beginSessionTransition`, which throws
2371
+ // `{ code: "busy" }` while another transition (compaction, handoff, fork,
2372
+ // another switch, …) owns the session. Resume is a UI action dispatched
2373
+ // through a void callback, so report that as status instead of rejecting a
2374
+ // promise nobody awaits. Admission-busy never acquired the lease, so the
2375
+ // owner's transient state — including the `compactionQueuedMessages` that
2376
+ // hold input typed during compaction — must survive untouched.
2377
+ const typed = error as { code?: unknown } | undefined;
2378
+ if (typed?.code === "busy") {
2379
+ this.ctx.showStatus("Another session operation is already in progress");
2380
+ return;
2381
+ }
2382
+ // A rejection can occur either before the transition mutates the live session
2383
+ // (for example, a rejecting `session_before_switch` hook) or after disconnect/
2384
+ // abort has begun. Preserve the current session's transient UI in the former
2385
+ // case; reconcile only state invalidated by a transition that actually mutated.
2386
+ // Reconciliation stays best-effort so cleanup failure never replaces the
2387
+ // original switch failure.
2388
+ if (transitionMutationStarted) {
2389
+ try {
2390
+ progressLease.clear();
2391
+ this.#clearTransientSessionUi({ restoreBackground: false, clearSpecializedLoaders: true });
2392
+ } catch {
2393
+ logger.warn("Resume transient-UI reconciliation failed after a session switch error", {
2394
+ classification: "resume-cleanup-failed",
2395
+ });
2396
+ }
2397
+ }
2398
+ throw error;
2399
+ }
2400
+ // `#clearTransientSessionUi` drops `compactionQueuedMessages`, so it must stay
2401
+ // untouched on the paths that never acquired the session-transition lease:
2402
+ // admission-busy above and managed-candidate preparation before it.
2403
+ //
2404
+ // A pre-mutation hook cancellation preserves the current session and all of
2405
+ // its foreground/retry UI. A rollback after disconnect/abort must clear it.
2406
+ if (!switched) {
2407
+ if (transitionMutationStarted) {
2408
+ progressLease.clear();
2409
+ this.#clearTransientSessionUi({ restoreBackground: false, clearSpecializedLoaders: true });
2410
+ }
2319
2411
  return;
2412
+ }
2413
+ progressLease.clear();
2414
+ this.#clearTransientSessionUi({ restoreBackground: false, clearSpecializedLoaders: true });
2320
2415
  const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
2321
2416
  if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
2322
2417
  this.#refreshSessionTerminalTitle();
@@ -2326,12 +2421,55 @@ export class SelectorController {
2326
2421
  switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
2327
2422
  );
2328
2423
  await this.ctx.reloadTodos();
2424
+ if (this.ctx.isStopped?.()) return;
2329
2425
  this.ctx.showStatus("Resumed session");
2426
+ this.#maybePromptResumeModelChoice();
2330
2427
  } finally {
2331
- progressLease.clear();
2428
+ progressLease?.clear();
2429
+ this.#resumeInFlight = false;
2430
+ releaseActivityIndicator();
2332
2431
  }
2333
2432
  }
2334
2433
 
2434
+ /**
2435
+ * When `session.resumeModelBehavior` is "ask", offer a one-shot choice after
2436
+ * resuming: keep the model the session was last using, or switch to whatever
2437
+ * `modelRoles.default` currently resolves to. No-op if the setting is unset,
2438
+ * the two models already match, or either model can't be resolved.
2439
+ */
2440
+ #maybePromptResumeModelChoice(): void {
2441
+ if (this.ctx.settings?.get("session.resumeModelBehavior") !== "ask") return;
2442
+ const sessionModel = this.ctx.session.model;
2443
+ const currentDefault = this.ctx.session.resolveConfiguredDefaultModel?.();
2444
+ if (!sessionModel || !currentDefault) return;
2445
+ if (sessionModel.provider === currentDefault.provider && sessionModel.id === currentDefault.id) return;
2446
+
2447
+ this.showSelector(done => {
2448
+ const selector = new HookSelectorComponent(
2449
+ `This session last used ${sessionModel.provider}/${sessionModel.id}.\n` +
2450
+ `Current default model is ${currentDefault.provider}/${currentDefault.id}.`,
2451
+ [`Keep ${sessionModel.id}`, `Use ${currentDefault.id}`],
2452
+ async (option: string) => {
2453
+ done();
2454
+ if (option === `Use ${currentDefault.id}`) {
2455
+ try {
2456
+ await this.ctx.session.setModel(currentDefault);
2457
+ this.ctx.showStatus(`Switched to ${currentDefault.provider}/${currentDefault.id}`);
2458
+ } catch (err) {
2459
+ this.ctx.showError(err instanceof Error ? err.message : String(err));
2460
+ }
2461
+ }
2462
+ this.ctx.ui.requestRender();
2463
+ },
2464
+ () => {
2465
+ done();
2466
+ this.ctx.ui.requestRender();
2467
+ },
2468
+ );
2469
+ return { component: selector, focus: selector };
2470
+ });
2471
+ }
2472
+
2335
2473
  async handleSessionDeleteCommand(): Promise<void> {
2336
2474
  const sessionFile = this.ctx.sessionManager.getSessionFile();
2337
2475
  if (!sessionFile) {