@gajae-code/coding-agent 0.12.1 → 0.12.4

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 (66) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  4. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
  5. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
  6. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
  7. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  8. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  9. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  10. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  11. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  12. package/dist/types/modes/interactive-mode.d.ts +9 -0
  13. package/dist/types/modes/theme/theme.d.ts +3 -1
  14. package/dist/types/modes/types.d.ts +32 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  16. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
  17. package/dist/types/session/agent-session.d.ts +2 -0
  18. package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
  19. package/dist/types/task/index.d.ts +4 -0
  20. package/dist/types/task/output-manager.d.ts +1 -0
  21. package/dist/types/task/receipt.d.ts +2 -0
  22. package/dist/types/tools/index.d.ts +2 -0
  23. package/package.json +8 -8
  24. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  25. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  26. package/scripts/run-acp-conformance.ts +5 -0
  27. package/src/async/job-manager.ts +22 -20
  28. package/src/config/provider-ranking.ts +1 -0
  29. package/src/config/settings-schema.ts +2 -2
  30. package/src/debug/index.ts +42 -17
  31. package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
  32. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
  33. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
  34. package/src/gjc-runtime/launch-worktree.ts +103 -1
  35. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  36. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  37. package/src/gjc-runtime/ralplan-runtime.ts +80 -10
  38. package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
  39. package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
  40. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  41. package/src/gjc-runtime/workflow-manifest.ts +17 -2
  42. package/src/internal-urls/docs-index.generated.ts +2 -2
  43. package/src/modes/DESIGN.md +53 -0
  44. package/src/modes/components/irc-sidebar.ts +30 -1
  45. package/src/modes/components/tool-status-header.ts +0 -1
  46. package/src/modes/controllers/command-controller.ts +38 -34
  47. package/src/modes/controllers/event-controller.ts +27 -16
  48. package/src/modes/controllers/extension-ui-controller.ts +186 -34
  49. package/src/modes/controllers/selector-controller.ts +115 -22
  50. package/src/modes/interactive-mode.ts +189 -62
  51. package/src/modes/jobs-observer.ts +1 -1
  52. package/src/modes/theme/theme.ts +11 -9
  53. package/src/modes/types.ts +88 -0
  54. package/src/sdk/broker/lifecycle.ts +18 -13
  55. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  56. package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
  57. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  58. package/src/sdk/session.ts +1 -0
  59. package/src/session/agent-session.ts +22 -1
  60. package/src/session/internal/managed-session-scope.ts +158 -107
  61. package/src/session/internal/managed-session-storage.ts +445 -57
  62. package/src/session/session-storage.ts +29 -44
  63. package/src/task/index.ts +257 -31
  64. package/src/task/output-manager.ts +24 -14
  65. package/src/task/receipt.ts +11 -1
  66. package/src/tools/index.ts +2 -0
@@ -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,
@@ -167,7 +173,7 @@ import type { JobsObserver } from "../jobs-observer";
167
173
  import type { SessionObserverRegistry } from "../session-observer-registry";
168
174
  import type { TasksAggregator } from "../tasks-aggregator";
169
175
  import type { TranscriptItemRegistry } from "../transcript-item-registry";
170
- import { acquireResumeProgressLease } from "../utils/ui-helpers";
176
+ import { acquireResumeProgressLease, type ResumeProgressLease } from "../utils/ui-helpers";
171
177
 
172
178
  const CALLBACK_SERVER_PROVIDERS = new Set<string>([
173
179
  "anthropic",
@@ -197,7 +203,6 @@ export function buildStatusLineSettings(settingsInstance: Settings): StatusLineS
197
203
  rightSegments: settingsInstance.get("statusLine.rightSegments"),
198
204
  separator: settingsInstance.get("statusLine.separator"),
199
205
  showHookStatus: settingsInstance.get("statusLine.showHookStatus"),
200
- showActionHints: settingsInstance.get("statusLine.showActionHints"),
201
206
  sessionAccent: settingsInstance.get("statusLine.sessionAccent"),
202
207
  maxRows: settingsInstance.get("statusLine.maxRows"),
203
208
  segmentOptions: settingsInstance.get("statusLine.segmentOptions"),
@@ -821,7 +826,9 @@ export class SelectorController {
821
826
  * @param create Factory that receives a `done` callback and returns the component and focus target
822
827
  */
823
828
  showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
829
+ if (this.ctx.isStopped?.()) return;
824
830
  const done = () => {
831
+ if (this.ctx.isStopped?.()) return;
825
832
  // Prefer the pet-aware composer restore (InteractiveMode.restoreComposer); fall back
826
833
  // to a plain editor swap for contexts that predate it (e.g. lightweight test doubles).
827
834
  if (typeof this.ctx.restoreComposer === "function") {
@@ -1566,7 +1573,8 @@ export class SelectorController {
1566
1573
  this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
1567
1574
  break;
1568
1575
  case "theme": {
1569
- 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;
1570
1578
  this.ctx.statusLine.invalidate();
1571
1579
  this.ctx.updateEditorTopBorder();
1572
1580
  this.ctx.ui.invalidate();
@@ -1626,13 +1634,16 @@ export class SelectorController {
1626
1634
  this.ctx.session.agent.repetitionPenalty = repetitionPenalty >= 0 ? repetitionPenalty : undefined;
1627
1635
  break;
1628
1636
  }
1637
+ case "statusLine.showActionHints": {
1638
+ this.ctx.updateEditorChrome();
1639
+ break;
1640
+ }
1629
1641
  case "statusLinePreset":
1630
1642
  case "statusLine.preset":
1631
1643
  case "statusLineSeparator":
1632
1644
  case "statusLine.separator":
1633
1645
  case "statusLineShowHooks":
1634
1646
  case "statusLine.showHookStatus":
1635
- case "statusLine.showActionHints":
1636
1647
  case "statusLine.sessionAccent":
1637
1648
  case "statusLine.maxRows":
1638
1649
  case "statusLine.leftSegments":
@@ -2138,12 +2149,14 @@ export class SelectorController {
2138
2149
 
2139
2150
  // Set up escape handler and loader if summarizing
2140
2151
  let summaryLoader: Loader | undefined;
2152
+ let releaseActivityIndicator: (() => void) | undefined;
2141
2153
  const originalOnEscape = this.ctx.editor.onEscape;
2142
2154
 
2143
2155
  if (wantsSummary) {
2144
2156
  this.ctx.editor.onEscape = () => {
2145
2157
  this.ctx.session.abortBranchSummary();
2146
2158
  };
2159
+ releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
2147
2160
  this.ctx.chatContainer.addChild(new Spacer(1));
2148
2161
  summaryLoader = new Loader(
2149
2162
  this.ctx.ui,
@@ -2161,6 +2174,7 @@ export class SelectorController {
2161
2174
  summarize: wantsSummary,
2162
2175
  customInstructions,
2163
2176
  });
2177
+ if (this.ctx.isStopped?.()) return;
2164
2178
 
2165
2179
  if (result.aborted) {
2166
2180
  // Summarization aborted - re-show tree selector
@@ -2176,18 +2190,21 @@ export class SelectorController {
2176
2190
  // Update UI — pass the context built by navigateTree to skip a second O(N) walk.
2177
2191
  this.ctx.rebuildInitialMessages("reconcile-same-transcript", result.sessionContext);
2178
2192
  await this.ctx.reloadTodos();
2193
+ if (this.ctx.isStopped?.()) return;
2179
2194
  if (result.editorText && !this.ctx.editor.getText().trim()) {
2180
2195
  this.ctx.editor.setText(result.editorText);
2181
2196
  }
2182
2197
  this.ctx.showStatus("Navigated to selected point");
2183
2198
  } catch (error) {
2199
+ if (this.ctx.isStopped?.()) return;
2184
2200
  this.ctx.showError(error instanceof Error ? error.message : String(error));
2185
2201
  } finally {
2186
2202
  if (summaryLoader) {
2187
2203
  summaryLoader.stop();
2188
- this.ctx.statusContainer.clear();
2204
+ if (!this.ctx.isStopped?.()) this.ctx.statusContainer.clear();
2189
2205
  }
2190
- this.ctx.editor.onEscape = originalOnEscape;
2206
+ if (!this.ctx.isStopped?.()) this.ctx.editor.onEscape = originalOnEscape;
2207
+ releaseActivityIndicator?.();
2191
2208
  }
2192
2209
  },
2193
2210
  () => {
@@ -2206,6 +2223,7 @@ export class SelectorController {
2206
2223
 
2207
2224
  async showSessionSelector(): Promise<void> {
2208
2225
  const sessions = await this.ctx.sessionManager.listForResumePickerReadOnly();
2226
+ if (this.ctx.isStopped?.()) return;
2209
2227
  this.showSelector(done => {
2210
2228
  const selector = new SessionSelectorComponent(
2211
2229
  sessions,
@@ -2240,12 +2258,9 @@ export class SelectorController {
2240
2258
  });
2241
2259
  }
2242
2260
 
2243
- #clearTransientSessionUi(): void {
2244
- if (this.ctx.loadingAnimation) {
2245
- this.ctx.loadingAnimation.stop();
2246
- this.ctx.loadingAnimation = undefined;
2247
- }
2248
- 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 });
2249
2264
  this.ctx.pendingMessagesContainer.clear();
2250
2265
  this.ctx.compactionQueuedMessages = [];
2251
2266
  this.ctx.streamingComponent = undefined;
@@ -2278,6 +2293,7 @@ export class SelectorController {
2278
2293
  }
2279
2294
 
2280
2295
  const detached = await this.ctx.session.newSession();
2296
+ if (this.ctx.isStopped?.()) return false;
2281
2297
  if (!detached) {
2282
2298
  return false;
2283
2299
  }
@@ -2285,28 +2301,50 @@ export class SelectorController {
2285
2301
 
2286
2302
  this.#refreshSessionTerminalTitle();
2287
2303
 
2288
- this.#clearTransientSessionUi();
2304
+ this.#clearTransientSessionUi({ clearSpecializedLoaders: true });
2289
2305
  this.ctx.statusLine.invalidate();
2290
2306
  this.ctx.statusLine.setSessionStartTime(Date.now());
2291
2307
  this.ctx.updateEditorTopBorder();
2292
2308
  this.ctx.updateEditorBorderColor();
2293
2309
  this.ctx.rebuildInitialMessages("replace-identity");
2294
2310
  await this.ctx.reloadTodos();
2311
+ if (this.ctx.isStopped?.()) return false;
2295
2312
  this.ctx.ui.requestRender();
2296
2313
  return true;
2297
2314
  }
2298
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
+
2299
2326
  async handleResumeSession(sessionPath: string): Promise<void> {
2300
- const previousSessionId = this.ctx.sessionManager.getSessionId();
2301
- this.#clearTransientSessionUi();
2302
- 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;
2303
2337
  try {
2338
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
2339
+ progressLease = acquireResumeProgressLease(this.ctx);
2304
2340
  await progressLease.committed;
2341
+ if (this.ctx.isStopped?.()) return;
2305
2342
  const migrationPolicy =
2306
2343
  this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
2307
2344
  let writableSessionPath = sessionPath;
2308
2345
  if (this.ctx.sessionManager.isManagedDestination()) {
2309
2346
  const inspection = await SessionManager.inspectSessionTailReadOnly(sessionPath);
2347
+ if (this.ctx.isStopped?.()) return;
2310
2348
  if (inspection.kind === "error")
2311
2349
  throw new Error(`Could not inspect selected session: ${inspection.reason}`);
2312
2350
  writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForStrictAdoption(
@@ -2314,14 +2352,66 @@ export class SelectorController {
2314
2352
  migrationPolicy,
2315
2353
  inspection.identity,
2316
2354
  );
2355
+ if (this.ctx.isStopped?.()) return;
2317
2356
  }
2318
2357
  // Switch session via AgentSession (emits hook and tool session events)
2319
- if (
2320
- !(await this.ctx.session.switchSession(writableSessionPath, {
2358
+ let switched: boolean;
2359
+ let transitionMutationStarted = false;
2360
+ try {
2361
+ switched = await this.ctx.session.switchSession(writableSessionPath, {
2321
2362
  transition: { origin: INTERACTIVE_SELECTOR_RESUME_ORIGIN },
2322
- }))
2323
- )
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
+ }
2324
2411
  return;
2412
+ }
2413
+ progressLease.clear();
2414
+ this.#clearTransientSessionUi({ restoreBackground: false, clearSpecializedLoaders: true });
2325
2415
  const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
2326
2416
  if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
2327
2417
  this.#refreshSessionTerminalTitle();
@@ -2331,10 +2421,13 @@ export class SelectorController {
2331
2421
  switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
2332
2422
  );
2333
2423
  await this.ctx.reloadTodos();
2424
+ if (this.ctx.isStopped?.()) return;
2334
2425
  this.ctx.showStatus("Resumed session");
2335
2426
  this.#maybePromptResumeModelChoice();
2336
2427
  } finally {
2337
- progressLease.clear();
2428
+ progressLease?.clear();
2429
+ this.#resumeInFlight = false;
2430
+ releaseActivityIndicator();
2338
2431
  }
2339
2432
  }
2340
2433