@gajae-code/coding-agent 0.16.0 → 0.16.3

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 (164) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/update-cli.d.ts +1 -0
  5. package/dist/types/commands/sdk.d.ts +2 -0
  6. package/dist/types/config/billing-path.d.ts +25 -0
  7. package/dist/types/config/config-schema-version.d.ts +13 -1
  8. package/dist/types/config/model-registry.d.ts +16 -0
  9. package/dist/types/config/settings-schema.d.ts +17 -7
  10. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  11. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  12. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  15. package/dist/types/modes/components/model-selector.d.ts +4 -0
  16. package/dist/types/modes/components/session-selector.d.ts +1 -1
  17. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  18. package/dist/types/modes/interactive-mode.d.ts +16 -2
  19. package/dist/types/modes/theme/theme.d.ts +2 -1
  20. package/dist/types/modes/types.d.ts +2 -0
  21. package/dist/types/sdk/broker/broker.d.ts +2 -1
  22. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
  25. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  26. package/dist/types/sdk/broker/state-version.d.ts +17 -0
  27. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  28. package/dist/types/sdk/bus/index.d.ts +3 -1
  29. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  30. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  31. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  32. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  33. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  34. package/dist/types/sdk/cli/rows.d.ts +15 -2
  35. package/dist/types/sdk/cli/session-cli.d.ts +2 -1
  36. package/dist/types/sdk/host/events.d.ts +2 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  39. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  40. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  41. package/dist/types/sdk/host/session-runtime.d.ts +18 -17
  42. package/dist/types/sdk/lifecycle-session.d.ts +1 -1
  43. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  44. package/dist/types/sdk/receipt-state.d.ts +2 -0
  45. package/dist/types/sdk/router/session-router.d.ts +4 -0
  46. package/dist/types/sdk/turn-result.d.ts +6 -0
  47. package/dist/types/session/agent-session.d.ts +47 -7
  48. package/dist/types/session/fold-coordinator.d.ts +29 -2
  49. package/dist/types/session/session-manager.d.ts +14 -2
  50. package/dist/types/tools/ask.d.ts +1 -0
  51. package/dist/types/tools/bash.d.ts +12 -0
  52. package/dist/types/tools/browser/launch.d.ts +10 -0
  53. package/dist/types/tools/browser/registry.d.ts +8 -0
  54. package/dist/types/tools/index.d.ts +4 -10
  55. package/dist/types/tools/job.d.ts +3 -1
  56. package/dist/types/utils/external-editor.d.ts +12 -0
  57. package/examples/extensions/README.md +1 -1
  58. package/examples/extensions/pirate.ts +7 -4
  59. package/package.json +7 -7
  60. package/scripts/generate-sdk-operation-inventory.ts +7 -1
  61. package/src/async/job-manager.ts +107 -9
  62. package/src/browser-backend.ts +29 -0
  63. package/src/cli/plugin-cli.ts +29 -0
  64. package/src/cli/update-cli.ts +63 -12
  65. package/src/commands/sdk.ts +22 -7
  66. package/src/config/billing-path.ts +42 -0
  67. package/src/config/config-schema-version.ts +18 -1
  68. package/src/config/model-preset-registry.ts +177 -18
  69. package/src/config/model-registry.ts +268 -42
  70. package/src/config/settings-schema.ts +19 -7
  71. package/src/config/settings.ts +46 -15
  72. package/src/coordinator-mcp/projection-scan.ts +6 -6
  73. package/src/coordinator-mcp/server.ts +21 -1
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/extensibility/extensions/runner.ts +27 -0
  76. package/src/extensibility/extensions/types.ts +8 -3
  77. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  78. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  79. package/src/extensibility/skills.ts +22 -12
  80. package/src/gjc-runtime/launch-worktree.ts +285 -4
  81. package/src/gjc-runtime/session-state-lock.ts +156 -32
  82. package/src/gjc-runtime/session-state-sidecar.ts +10 -17
  83. package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
  84. package/src/internal-urls/docs-index.generated.ts +11 -11
  85. package/src/main.ts +4 -1
  86. package/src/modes/acp/acp-agent.ts +34 -19
  87. package/src/modes/components/model-selector.ts +76 -8
  88. package/src/modes/components/session-selector.ts +23 -15
  89. package/src/modes/components/sessions-dashboard.ts +8 -5
  90. package/src/modes/controllers/command-controller.ts +1 -1
  91. package/src/modes/controllers/event-controller.ts +30 -4
  92. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  93. package/src/modes/controllers/input-controller.ts +2 -2
  94. package/src/modes/controllers/selector-controller.ts +10 -3
  95. package/src/modes/interactive-mode.ts +36 -12
  96. package/src/modes/prompt-action-autocomplete.ts +30 -4
  97. package/src/modes/runtime-init.ts +12 -9
  98. package/src/modes/theme/theme.ts +5 -0
  99. package/src/modes/types.ts +2 -0
  100. package/src/modes/utils/ui-helpers.ts +22 -3
  101. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  102. package/src/prompts/tools/ask.md +1 -1
  103. package/src/prompts/tools/bash.md +1 -0
  104. package/src/prompts/tools/job.md +1 -1
  105. package/src/sdk/acp/adapter.ts +6 -1
  106. package/src/sdk/broker/broker.ts +10 -1
  107. package/src/sdk/broker/ensure.ts +76 -3
  108. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  109. package/src/sdk/broker/lifecycle.ts +254 -34
  110. package/src/sdk/broker/spawn-substrate.ts +45 -2
  111. package/src/sdk/broker/startup-budget.ts +144 -2
  112. package/src/sdk/broker/state-version.ts +17 -0
  113. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  114. package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
  115. package/src/sdk/bus/discord-daemon.ts +50 -10
  116. package/src/sdk/bus/index.ts +33 -3
  117. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  118. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  119. package/src/sdk/bus/notification-service.ts +22 -5
  120. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  121. package/src/sdk/bus/reconciliation-store.ts +55 -31
  122. package/src/sdk/bus/slack-conversation.ts +1 -1
  123. package/src/sdk/bus/slack-daemon.ts +72 -38
  124. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  125. package/src/sdk/cli/rows.ts +75 -7
  126. package/src/sdk/cli/session-cli.ts +68 -35
  127. package/src/sdk/host/control/dispatch.ts +2 -2
  128. package/src/sdk/host/events.ts +26 -2
  129. package/src/sdk/host/host.ts +6 -1
  130. package/src/sdk/host/query/cursor.ts +52 -7
  131. package/src/sdk/host/query/handlers.ts +33 -12
  132. package/src/sdk/host/query/revision-store.ts +12 -0
  133. package/src/sdk/host/session-runtime.test.ts +497 -31
  134. package/src/sdk/host/session-runtime.ts +387 -54
  135. package/src/sdk/host/surface-policy.ts +3 -3
  136. package/src/sdk/lifecycle-session.ts +7 -2
  137. package/src/sdk/prompt-deadline-manager.ts +2 -1
  138. package/src/sdk/protocol/operation-inventory.generated.json +54 -3
  139. package/src/sdk/protocol/operation-registry.ts +1 -1
  140. package/src/sdk/receipt-state.ts +10 -0
  141. package/src/sdk/router/session-router.ts +210 -94
  142. package/src/sdk/session.ts +59 -41
  143. package/src/sdk/turn-result.ts +22 -0
  144. package/src/session/agent-session.ts +1033 -260
  145. package/src/session/fold-coordinator.ts +40 -3
  146. package/src/session/session-manager.ts +517 -77
  147. package/src/slash-commands/builtin-registry.ts +41 -0
  148. package/src/task/executor.ts +9 -4
  149. package/src/tools/ask-contract.test.ts +64 -0
  150. package/src/tools/ask-contract.ts +36 -1
  151. package/src/tools/ask.ts +10 -8
  152. package/src/tools/bash-interactive.ts +11 -2
  153. package/src/tools/bash.ts +158 -33
  154. package/src/tools/browser/launch.ts +37 -0
  155. package/src/tools/browser/registry.ts +99 -9
  156. package/src/tools/cron.ts +6 -4
  157. package/src/tools/descriptors.test.ts +15 -0
  158. package/src/tools/descriptors.ts +5 -1
  159. package/src/tools/index.ts +4 -6
  160. package/src/tools/job.ts +4 -1
  161. package/src/tools/monitor.ts +12 -9
  162. package/src/tools/resolve.ts +30 -10
  163. package/src/tools/tool-catalog.generated.ts +2 -2
  164. package/src/utils/external-editor.ts +86 -1
@@ -68,9 +68,25 @@ export function jobElapsedMs(job: Pick<AsyncJob, "startTime" | "endTime">, now:
68
68
  return Math.max(0, (job.endTime ?? now) - job.startTime);
69
69
  }
70
70
 
71
+ /**
72
+ * Why a foreground wait left the foreground. Every fold path names its
73
+ * trigger so job snapshots and SDK clients can tell a user steer from the
74
+ * chord, the auto-background timer, or the explicit `bash.background` control.
75
+ */
76
+ export type FoldReason = "steer" | "chord" | "timer" | "sdk_control";
77
+
78
+ /** A fold observed by the manager; published to SDK clients as `bash_folded`. */
79
+ export interface JobFoldEvent {
80
+ jobId: string;
81
+ generation: string;
82
+ reason: FoldReason;
83
+ }
84
+
71
85
  export interface AsyncJobMetadata {
72
86
  /** Set once a foreground wait has been folded, so surfacing can show folded work. */
73
87
  backgrounded?: boolean;
88
+ /** Why a foreground wait was folded; absent for jobs started directly in the background (`async: true`). */
89
+ foldReason?: FoldReason;
74
90
  subagent?: {
75
91
  id: string;
76
92
  agent: string;
@@ -271,6 +287,7 @@ interface AsyncJobDelivery {
271
287
  label: string;
272
288
  status: AsyncJob["status"];
273
289
  backgrounded: boolean;
290
+ foldReason?: FoldReason;
274
291
  text: string;
275
292
  originalBytes?: number;
276
293
  truncated?: boolean;
@@ -290,6 +307,7 @@ interface AsyncJobReceiptClaim {
290
307
  label: string;
291
308
  status: AsyncJob["status"];
292
309
  backgrounded: boolean;
310
+ foldReason?: FoldReason;
293
311
  ownerId?: string;
294
312
  }
295
313
 
@@ -331,6 +349,8 @@ export interface AsyncJobSnapshotEntry {
331
349
  status: AsyncJob["status"];
332
350
  generation: string;
333
351
  backgrounded: boolean;
352
+ /** Present when `backgrounded` was set by a fold; absent for direct background starts. */
353
+ foldReason?: FoldReason;
334
354
  deliveryState: JobDeliveryState;
335
355
  }
336
356
 
@@ -630,6 +650,7 @@ export class AsyncJobManager {
630
650
  }
631
651
 
632
652
  readonly #jobs = new Map<string, AsyncJob>();
653
+ #retainedDisposalCompletion: Promise<void> = Promise.resolve();
633
654
  readonly #deliveries: AsyncJobDelivery[] = [];
634
655
  readonly #inFlightDeliveries: AsyncJobDelivery[] = [];
635
656
  readonly #suppressedDeliveries = new Set<string>();
@@ -653,7 +674,6 @@ export class AsyncJobManager {
653
674
  readonly #maxDeadLetterOverflowOwners: number;
654
675
  #deliveryLoop: Promise<void> | undefined;
655
676
  #disposed = false;
656
- #disposing = false;
657
677
  #runningOwnerCleanups = false;
658
678
  readonly #subagentRecords = new Map<string, SubagentRecord>();
659
679
  readonly #terminalEvents = new Map<string, TerminalEvent>();
@@ -996,6 +1016,8 @@ export class AsyncJobManager {
996
1016
  };
997
1017
  }
998
1018
 
1019
+ readonly #foldListeners = new Set<(event: JobFoldEvent) => void>();
1020
+
999
1021
  #notifyChange(): void {
1000
1022
  for (const cb of this.#changeListeners) {
1001
1023
  try {
@@ -2307,6 +2329,7 @@ export class AsyncJobManager {
2307
2329
  status: job.status,
2308
2330
  generation: job.generation,
2309
2331
  backgrounded: job.metadata?.backgrounded === true,
2332
+ foldReason: job.metadata?.foldReason,
2310
2333
  deliveryState,
2311
2334
  };
2312
2335
  });
@@ -2329,6 +2352,7 @@ export class AsyncJobManager {
2329
2352
  status: delivery.status,
2330
2353
  generation: delivery.generation,
2331
2354
  backgrounded: delivery.backgrounded,
2355
+ foldReason: delivery.foldReason,
2332
2356
  deliveryState: "pending",
2333
2357
  });
2334
2358
  }
@@ -2344,6 +2368,7 @@ export class AsyncJobManager {
2344
2368
  status: delivery.status,
2345
2369
  generation: delivery.generation,
2346
2370
  backgrounded: delivery.backgrounded,
2371
+ foldReason: delivery.foldReason,
2347
2372
  deliveryState: "pending",
2348
2373
  });
2349
2374
  }
@@ -2359,6 +2384,7 @@ export class AsyncJobManager {
2359
2384
  status: claim.status,
2360
2385
  generation: claim.generation,
2361
2386
  backgrounded: claim.backgrounded,
2387
+ foldReason: claim.foldReason,
2362
2388
  deliveryState: "pending",
2363
2389
  });
2364
2390
  }
@@ -2408,6 +2434,7 @@ export class AsyncJobManager {
2408
2434
  label: job.label,
2409
2435
  status: job.status,
2410
2436
  backgrounded: job.metadata?.backgrounded === true,
2437
+ foldReason: job.metadata?.foldReason,
2411
2438
  text,
2412
2439
  attempt: 0,
2413
2440
  nextAttemptAt: Date.now(),
@@ -2426,6 +2453,7 @@ export class AsyncJobManager {
2426
2453
  label: job.label,
2427
2454
  status: job.status,
2428
2455
  backgrounded: job.metadata?.backgrounded === true,
2456
+ foldReason: job.metadata?.foldReason,
2429
2457
  ownerId: job.ownerId,
2430
2458
  });
2431
2459
  this.#notifyChange();
@@ -2632,12 +2660,26 @@ export class AsyncJobManager {
2632
2660
  }
2633
2661
 
2634
2662
  /**
2635
- * Mark a running job as backgrounded (folded out of its foreground call).
2636
- * Read by getJobsSnapshot so folded work stays visible; safe to call twice.
2663
+ * Subscribe to fold events. Fires once per successful `markBackgrounded`
2664
+ * transition (never on the idempotent repeat), so a subscriber can publish
2665
+ * exactly one `bash_folded` frame per fold. Listener errors are isolated.
2637
2666
  */
2638
- markBackgrounded(jobId: string, generation: string): boolean {
2667
+ onFold(cb: (event: JobFoldEvent) => void): () => void {
2668
+ this.#foldListeners.add(cb);
2669
+ return () => {
2670
+ this.#foldListeners.delete(cb);
2671
+ };
2672
+ }
2673
+
2674
+ /**
2675
+ * Mark a job that was launched directly in the background (`bash` with
2676
+ * `async: true`). It never had a foreground wait, so this is not a fold: no
2677
+ * `foldReason` is recorded and no fold listener fires.
2678
+ */
2679
+ markStartedInBackground(jobId: string, generation: string): boolean {
2639
2680
  const job = this.#jobs.get(jobId);
2640
2681
  if (!job || job.generation !== generation) return false;
2682
+ if (job.metadata?.backgrounded === true) return true;
2641
2683
  job.metadata = { ...job.metadata, backgrounded: true };
2642
2684
  for (const delivery of [...this.#deliveries, ...this.#inFlightDeliveries]) {
2643
2685
  if (delivery.jobId === jobId && delivery.generation === generation) delivery.backgrounded = true;
@@ -2646,6 +2688,37 @@ export class AsyncJobManager {
2646
2688
  return true;
2647
2689
  }
2648
2690
 
2691
+ /**
2692
+ * Mark a running job as backgrounded (folded out of its foreground call).
2693
+ * Read by getJobsSnapshot so folded work stays visible; safe to call twice.
2694
+ * The first transition records `reason` and notifies fold listeners; a
2695
+ * repeat keeps the original reason and emits nothing.
2696
+ */
2697
+ markBackgrounded(jobId: string, generation: string, reason: FoldReason): boolean {
2698
+ const job = this.#jobs.get(jobId);
2699
+ if (!job || job.generation !== generation) return false;
2700
+ if (job.metadata?.backgrounded === true) return true;
2701
+ job.metadata = { ...job.metadata, backgrounded: true, foldReason: reason };
2702
+ for (const delivery of [...this.#deliveries, ...this.#inFlightDeliveries]) {
2703
+ if (delivery.jobId === jobId && delivery.generation === generation) {
2704
+ delivery.backgrounded = true;
2705
+ delivery.foldReason = reason;
2706
+ }
2707
+ }
2708
+ this.#notifyChange();
2709
+ const event: JobFoldEvent = { jobId, generation, reason };
2710
+ for (const cb of this.#foldListeners) {
2711
+ try {
2712
+ cb(event);
2713
+ } catch (error) {
2714
+ logger.warn("Async job fold listener failed", {
2715
+ error: error instanceof Error ? error.message : String(error),
2716
+ });
2717
+ }
2718
+ }
2719
+ return true;
2720
+ }
2721
+
2649
2722
  async waitForAll(): Promise<void> {
2650
2723
  await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
2651
2724
  }
@@ -2707,6 +2780,17 @@ export class AsyncJobManager {
2707
2780
  // registration keeps the original protection (cleanups cannot register fresh
2708
2781
  // work) without disabling delivery. Errors in cleanups are logged, never
2709
2782
  // escalated.
2783
+ const unsettledJobPromises = new Set<Promise<void>>();
2784
+ const jobIdsByPromise = new Map<Promise<void>, string>();
2785
+ for (const job of this.#jobs.values()) {
2786
+ const promise = job.promise;
2787
+ unsettledJobPromises.add(promise);
2788
+ jobIdsByPromise.set(promise, job.id);
2789
+ void promise.then(
2790
+ () => unsettledJobPromises.delete(promise),
2791
+ () => unsettledJobPromises.delete(promise),
2792
+ );
2793
+ }
2710
2794
  this.#registrationClosed = true;
2711
2795
  this.#clearEvictionTimers();
2712
2796
  this.#runningOwnerCleanups = true;
@@ -2715,7 +2799,6 @@ export class AsyncJobManager {
2715
2799
  } finally {
2716
2800
  this.#runningOwnerCleanups = false;
2717
2801
  }
2718
- this.#disposing = true;
2719
2802
  this.cancelAll();
2720
2803
  for (const tombstone of this.#monitorTombstones.values()) {
2721
2804
  try {
@@ -2734,13 +2817,22 @@ export class AsyncJobManager {
2734
2817
  const remainingDeliveryMs = Math.max(0, disposalDeadline - Date.now());
2735
2818
  const drained = await this.drainDeliveries({ timeoutMs: remainingDeliveryMs });
2736
2819
  if (!drained) this.#projectUndeliveredDisposalFailures();
2737
- const disposalCompleted = waitResult.completed && drained;
2820
+ const retainedAuthority = new Set<Promise<void>>(unsettledJobPromises);
2821
+ for (const delivery of this.#inFlightDeliveries) {
2822
+ if (delivery.promise) retainedAuthority.add(delivery.promise);
2823
+ }
2824
+ this.#retainedDisposalCompletion = Promise.allSettled(retainedAuthority).then(() => {});
2825
+ const physicalStuckJobIds = [...unsettledJobPromises]
2826
+ .map(promise => jobIdsByPromise.get(promise))
2827
+ .filter((id): id is string => id !== undefined);
2828
+ const stuckJobIds = [...new Set([...waitResult.stuckJobIds, ...physicalStuckJobIds])];
2829
+ const disposalCompleted = waitResult.completed && drained && retainedAuthority.size === 0;
2738
2830
  this.#lastDisposeDiagnostics = {
2739
- stuckJobIds: waitResult.stuckJobIds,
2831
+ stuckJobIds,
2740
2832
  deliveriesDrained: disposalCompleted,
2741
2833
  };
2742
- if (waitResult.stuckJobIds.length > 0) {
2743
- logger.warn("Async job manager dispose timed out waiting for jobs", { stuckJobIds: waitResult.stuckJobIds });
2834
+ if (stuckJobIds.length > 0) {
2835
+ logger.warn("Async job manager dispose timed out waiting for jobs", { stuckJobIds });
2744
2836
  }
2745
2837
  this.#clearEvictionTimers();
2746
2838
  this.#disposed = true;
@@ -2772,9 +2864,14 @@ export class AsyncJobManager {
2772
2864
  this.#ownerSubagentShutdownLeases.clear();
2773
2865
  this.#notifyChange();
2774
2866
  this.#changeListeners.clear();
2867
+ this.#foldListeners.clear();
2775
2868
  return disposalCompleted;
2776
2869
  }
2777
2870
 
2871
+ awaitRetainedDisposalCompletion(): Promise<void> {
2872
+ return this.#retainedDisposalCompletion;
2873
+ }
2874
+
2778
2875
  #projectUndeliveredDisposalFailures(): void {
2779
2876
  const retained = [...this.#deliveries, ...this.#inFlightDeliveries];
2780
2877
  for (const delivery of retained) {
@@ -3145,6 +3242,7 @@ export class AsyncJobManager {
3145
3242
  label: job.label,
3146
3243
  status: job.status,
3147
3244
  backgrounded: job.metadata?.backgrounded === true,
3245
+ foldReason: job.metadata?.foldReason,
3148
3246
  text: deliveryText.text,
3149
3247
  originalBytes: deliveryText.originalBytes,
3150
3248
  truncated: deliveryText.truncated,
@@ -0,0 +1,29 @@
1
+ import browserAsidePrompt from "./prompts/agent-fragments/browser-aside.md" with { type: "text" };
2
+
3
+ export type BrowserBackendId = "native" | "aside";
4
+
5
+ export interface BrowserBackend {
6
+ readonly id: BrowserBackendId;
7
+ readonly exposesBuiltinTool: boolean;
8
+ readonly developerInstructions?: string;
9
+ }
10
+
11
+ interface BrowserBackendSettingsSource {
12
+ get(key: string): unknown;
13
+ }
14
+
15
+ const nativeBrowserBackend: BrowserBackend = {
16
+ id: "native",
17
+ exposesBuiltinTool: true,
18
+ };
19
+
20
+ const asideBrowserBackend: BrowserBackend = {
21
+ id: "aside",
22
+ exposesBuiltinTool: false,
23
+ developerInstructions: browserAsidePrompt.trim(),
24
+ };
25
+
26
+ /** Resolve the configured browser backend. Aside is strictly opt-in. */
27
+ export function resolveBrowserBackend(settings: BrowserBackendSettingsSource): BrowserBackend {
28
+ return settings.get("browser.backend") === "aside" ? asideBrowserBackend : nativeBrowserBackend;
29
+ }
@@ -886,11 +886,40 @@ async function handleLink(manager: PluginManager, paths: string[], flags: { json
886
886
  }
887
887
  }
888
888
 
889
+ async function collectMarketplaceDoctorChecks(): Promise<
890
+ Array<{ name: string; status: "ok" | "warning" | "error"; message: string }>
891
+ > {
892
+ const extra: Array<{ name: string; status: "ok" | "warning" | "error"; message: string }> = [];
893
+ try {
894
+ const mktMgr = await makeMarketplaceManager();
895
+ const installed = await mktMgr.listInstalledPlugins();
896
+ for (const summary of installed) {
897
+ for (const entry of summary.entries) {
898
+ let exists = false;
899
+ try {
900
+ const { statSync } = await import("node:fs");
901
+ statSync(entry.installPath);
902
+ exists = true;
903
+ } catch {}
904
+ if (!exists) {
905
+ extra.push({
906
+ name: `marketplace:${summary.id}:${entry.scope}`,
907
+ status: "error",
908
+ message: `Marketplace plugin "${summary.id}" (${entry.scope}) references missing cache dir "${entry.installPath}" (v${entry.version}); reinstall or upgrade the plugin`,
909
+ });
910
+ }
911
+ }
912
+ }
913
+ } catch {}
914
+ return extra;
915
+ }
916
+
889
917
  async function handleDoctor(
890
918
  manager: PluginManager,
891
919
  flags: { json?: boolean; fix?: boolean; migratePlugins?: boolean },
892
920
  ): Promise<void> {
893
921
  const checks = await manager.doctor({ fix: flags.fix });
922
+ checks.push(...(await collectMarketplaceDoctorChecks()));
894
923
  try {
895
924
  const statuses = flags.migratePlugins
896
925
  ? await runGjcPluginMigrationPreflight(getProjectDir())
@@ -1150,6 +1150,67 @@ function formatRegistryProvenance(version: string, registry: string | undefined)
1150
1150
  return `Version ${version} was resolved from ${registry}; GitHub release assets may not exist for a version that was never published there.`;
1151
1151
  }
1152
1152
 
1153
+ interface ProcessReportWithLibc {
1154
+ header?: {
1155
+ glibcVersionRuntime?: unknown;
1156
+ };
1157
+ }
1158
+
1159
+ const GLIBC_VERSION_PATTERN = /^\d+\.\d+$/u;
1160
+
1161
+ function reportedGlibcVersion(value: unknown): string | undefined {
1162
+ if (typeof value !== "string") return undefined;
1163
+ const version = value.trim();
1164
+ return GLIBC_VERSION_PATTERN.test(version) ? version : undefined;
1165
+ }
1166
+
1167
+ function runtimeGlibcVersion(): string | undefined {
1168
+ try {
1169
+ const report = process.report?.getReport?.() as ProcessReportWithLibc | undefined;
1170
+ return reportedGlibcVersion(report?.header?.glibcVersionRuntime);
1171
+ } catch {
1172
+ return undefined;
1173
+ }
1174
+ }
1175
+
1176
+ function hasMuslLoader(): boolean {
1177
+ return (
1178
+ fs.existsSync("/lib/ld-musl-x86_64.so.1") ||
1179
+ fs.existsSync("/lib/ld-musl-aarch64.so.1") ||
1180
+ fs.existsSync("/lib/ld-musl-armhf.so.1")
1181
+ );
1182
+ }
1183
+
1184
+ type LinuxLibc = "glibc" | "musl" | "unknown";
1185
+
1186
+ function detectLinuxLibcFromSignals(glibcVersionRuntime: string | undefined, muslLoaderPresent: boolean): LinuxLibc {
1187
+ if (reportedGlibcVersion(glibcVersionRuntime) !== undefined) return "glibc";
1188
+ return muslLoaderPresent ? "musl" : "unknown";
1189
+ }
1190
+
1191
+ function detectLinuxLibc(): LinuxLibc {
1192
+ return detectLinuxLibcFromSignals(runtimeGlibcVersion(), hasMuslLoader());
1193
+ }
1194
+
1195
+ function unsupportedLinuxLibcReason(libc: LinuxLibc): string | undefined {
1196
+ if (libc === "glibc") return undefined;
1197
+ return libc === "musl"
1198
+ ? "Unsupported libc: musl. Prebuilt Linux binaries are glibc-only. See docs/install.md"
1199
+ : "Unable to verify an active glibc runtime. Prebuilt Linux binaries are glibc-only. See docs/install.md";
1200
+ }
1201
+
1202
+ function assertSupportedLinuxLibc(libc: LinuxLibc): void {
1203
+ const reason = unsupportedLinuxLibcReason(libc);
1204
+ if (reason) throw new Error(formatUnsupportedTargetMessage(reason));
1205
+ }
1206
+
1207
+ export function assertSupportedLinuxLibcForTest(
1208
+ glibcVersionRuntime: string | undefined,
1209
+ muslLoaderPresent: boolean,
1210
+ ): void {
1211
+ assertSupportedLinuxLibc(detectLinuxLibcFromSignals(glibcVersionRuntime, muslLoaderPresent));
1212
+ }
1213
+
1153
1214
  /**
1154
1215
  * Download a release binary to a target path, replacing an existing file.
1155
1216
  */
@@ -1158,18 +1219,8 @@ async function updateViaBinaryAt(
1158
1219
  expectedVersion: string,
1159
1220
  registry?: string,
1160
1221
  ): Promise<InstalledVersionVerification> {
1161
- if (
1162
- process.platform === "linux" &&
1163
- (fs.existsSync("/lib/ld-musl-x86_64.so.1") ||
1164
- fs.existsSync("/lib/ld-musl-aarch64.so.1") ||
1165
- fs.existsSync("/lib/ld-musl-armhf.so.1"))
1166
- ) {
1167
- throw new Error(
1168
- formatUnsupportedTargetMessage(
1169
- "Unsupported libc: musl. Prebuilt Linux binaries are glibc-only. See docs/install.md",
1170
- ),
1171
- );
1172
- }
1222
+ const linuxLibc = process.platform === "linux" ? detectLinuxLibc() : undefined;
1223
+ if (linuxLibc !== undefined) assertSupportedLinuxLibc(linuxLibc);
1173
1224
  const binaryName = getBinaryName();
1174
1225
  const url = buildReleaseBinaryUrl(expectedVersion);
1175
1226
  const registryNote = formatRegistryProvenance(expectedVersion, registry);
@@ -39,6 +39,7 @@ import {
39
39
  SdkStartupRollbackTracker,
40
40
  } from "../sdk/startup-capability";
41
41
  import { runSdkServe } from "../sdk/transport/serve-cli";
42
+ import { isSessionDisposalIncompleteError } from "../session/agent-session";
42
43
  import {
43
44
  type CapturedSessionTranscriptSnapshot,
44
45
  type ResumeSessionIdentity,
@@ -405,11 +406,13 @@ export async function runSessionHost(
405
406
  sleep?: (ms: number) => Promise<void>;
406
407
  cwd?: string;
407
408
  processIncarnation?: (pid: number) => string | undefined;
409
+ applyStartupModelProfiles?: typeof applyStartupModelProfiles;
408
410
  } = {},
409
411
  ): Promise<void> {
410
412
  const now = timing.now ?? Date.now;
411
413
  const sleep = timing.sleep ?? (async ms => await Bun.sleep(ms));
412
414
  const readIncarnation = timing.processIncarnation ?? processIncarnation;
415
+ const applyModelProfiles = timing.applyStartupModelProfiles ?? applyStartupModelProfiles;
413
416
  const request = readSessionLifecycleLaunchRequest(process.env.GJC_SDK_LIFECYCLE_REQUEST, now());
414
417
  const agentDir = process.env.GJC_AGENT_DIR;
415
418
  if (!agentDir) throw new Error("GJC_AGENT_DIR is required for sdk session-host-internal.");
@@ -599,22 +602,32 @@ export async function runSessionHost(
599
602
  throw created.failure;
600
603
  }
601
604
  const { session, capability, rollback, startDeferredMemoryBackend } = created;
605
+ let lifecycleTranscriptPath: string | undefined;
606
+ session.registerToolSessionTransitionCleanup(async () => {
607
+ await session.sessionManager.ensureOnDisk();
608
+ lifecycleTranscriptPath = session.sessionManager.getSessionFile();
609
+ });
602
610
  let sessionDisposal: Promise<void> | undefined;
603
611
  const disposeSession = (): Promise<void> => {
604
- sessionDisposal ??= session.dispose().catch(() => {});
612
+ sessionDisposal ??= (async () => {
613
+ try {
614
+ await session.dispose();
615
+ } catch (error) {
616
+ if (!isSessionDisposalIncompleteError(error)) throw error;
617
+ await session.awaitDisposeCompletion();
618
+ }
619
+ })();
605
620
  return sessionDisposal;
606
621
  };
607
622
  let disposal: Promise<LifecycleTranscriptEvidence | undefined> | undefined;
608
623
  const disposeAndCapture = (): Promise<LifecycleTranscriptEvidence | undefined> => {
609
624
  disposal ??= (async () => {
610
625
  await disposeSession();
626
+ if (!lifecycleTranscriptPath) return undefined;
611
627
  try {
612
- await session.sessionManager.ensureOnDisk();
613
- const transcriptPath = session.sessionManager.getSessionFile();
614
- if (!transcriptPath) return undefined;
615
628
  const [bytes, stat] = await Promise.all([
616
- fs.readFile(transcriptPath),
617
- fs.stat(transcriptPath, { bigint: true }),
629
+ fs.readFile(lifecycleTranscriptPath),
630
+ fs.stat(lifecycleTranscriptPath, { bigint: true }),
618
631
  ]);
619
632
  const digest = createHash("sha256").update(bytes).digest("hex");
620
633
  return {
@@ -708,12 +721,14 @@ export async function runSessionHost(
708
721
  const modelProfileStartup =
709
722
  process.env.GJC_SDK_TEST_HANG_MODEL_PROFILE === cwd
710
723
  ? new Promise<void>(() => {})
711
- : applyStartupModelProfiles({
724
+ : applyModelProfiles({
712
725
  session,
713
726
  settings: session.settings,
714
727
  modelRegistry: session.modelRegistry,
715
728
  parsedArgs: parsed,
716
729
  startupThinkingLevel,
730
+ preferCachedModels: true,
731
+ preferCachedDefaultProfile: true,
717
732
  });
718
733
  await beforeCutoff(modelProfileStartup);
719
734
  throwIfCutoff();
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Billing path for a resolved binding's credential source — audit-reversible.
3
+ * Derived from (model.api, credential-provenance) where the binding actually
4
+ * resolves credentials, not from a provider-name allowlist.
5
+ */
6
+ import type { Api } from "@gajae-code/ai/core";
7
+ import type { EffectiveProviderAuth } from "./provider-selection-policy";
8
+
9
+ /**
10
+ * Label shown in the selector row + active-model line. Only the category is
11
+ * disclosed (never the key material or project id).
12
+ */
13
+ export type BillingPathKind = "metered-api" | "cloud-project" | "bundled";
14
+
15
+ export interface BillingPath {
16
+ kind: BillingPathKind;
17
+ /** Stable per-credential-source key for one-time notice dismissal. */
18
+ dismissKey: string;
19
+ label: string;
20
+ }
21
+
22
+ /**
23
+ * Derive from the concrete binding (api + credential provenance).
24
+ * - oauth/keyless/unknown => bundled (plan/proxied/kNoAuth-like)
25
+ * - key + cloud-project api (google-vertex / bedrock) => cloud-project
26
+ * - key + any other api (including custom OpenAI-compatible) => metered-api
27
+ */
28
+ export function deriveBillingPath(
29
+ provider: string,
30
+ api: Api | string | undefined,
31
+ effectiveAuth: EffectiveProviderAuth,
32
+ ): BillingPath | undefined {
33
+ if (effectiveAuth === "oauth" || effectiveAuth === "keyless" || effectiveAuth === "unknown") return undefined;
34
+ if (effectiveAuth !== "key") return undefined;
35
+ const apiKind = typeof api === "string" ? api : undefined;
36
+ const cloudApis = new Set(["google-vertex", "bedrock-converse-stream"]);
37
+ const isCloud = apiKind ? cloudApis.has(apiKind) : false;
38
+ const kind: BillingPathKind = isCloud ? "cloud-project" : "metered-api";
39
+ const label = kind === "cloud-project" ? "cloud-project billing" : "metered API";
40
+ const dismissKey = `${kind}:${provider.trim().toLowerCase()}`;
41
+ return { kind, dismissKey, label };
42
+ }
@@ -7,4 +7,21 @@
7
7
  * this dependency-light module so the resolver can import it without pulling in
8
8
  * the TUI/AI-heavy settings schema.
9
9
  */
10
- export const CONFIG_SCHEMA_VERSION = 1;
10
+ export const CONFIG_SCHEMA_VERSION = 2;
11
+
12
+ /**
13
+ * Normalize a durable `configSchemaVersion` marker into a comparable version.
14
+ *
15
+ * Every version decision (future-schema detection, migration-pending state, and
16
+ * the ordered migration registry) must read the SAME normalized value: a
17
+ * malformed marker read as a future schema by one guard and as unmigrated by
18
+ * the next would skip legacy migrations while still re-running v0 transforms.
19
+ * A finite malformed value floors to the nearest completed version; a
20
+ * non-finite or non-numeric one carries no version information and normalizes
21
+ * to 0 (fully unmigrated). `undefined` stays `undefined` (no marker written).
22
+ */
23
+ export function normalizeConfigSchemaVersion(value: unknown): number | undefined {
24
+ if (value === undefined) return undefined;
25
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
26
+ return Math.max(0, Math.floor(value));
27
+ }