@gajae-code/coding-agent 0.13.0 → 0.13.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 (119) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/config/file-lock.d.ts +2 -0
  3. package/dist/types/config/keybindings.d.ts +2 -2
  4. package/dist/types/config/model-registry.d.ts +4 -0
  5. package/dist/types/config/models-config-schema.d.ts +6 -0
  6. package/dist/types/config/settings-schema.d.ts +17 -5
  7. package/dist/types/config/settings.d.ts +10 -1
  8. package/dist/types/edit/streaming.d.ts +5 -5
  9. package/dist/types/eval/py/kernel.d.ts +2 -2
  10. package/dist/types/eval/py/runtime.d.ts +5 -1
  11. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
  12. package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
  13. package/dist/types/modes/components/model-selector.d.ts +3 -2
  14. package/dist/types/runtime/process-lifecycle.d.ts +12 -31
  15. package/dist/types/sdk/broker/identity.d.ts +2 -1
  16. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
  17. package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
  18. package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  20. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +4 -0
  22. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
  23. package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
  26. package/dist/types/sdk/host/query/cursor.d.ts +4 -0
  27. package/dist/types/sdk/host/query/handlers.d.ts +6 -0
  28. package/dist/types/sdk/host/session-runtime.d.ts +7 -6
  29. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  30. package/dist/types/sdk/index.d.ts +1 -0
  31. package/dist/types/sdk/turn-result.d.ts +37 -0
  32. package/dist/types/session/agent-session.d.ts +8 -11
  33. package/dist/types/session/blob-store.d.ts +6 -3
  34. package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
  35. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
  36. package/dist/types/task/executor.d.ts +4 -7
  37. package/dist/types/tools/index.d.ts +1 -1
  38. package/dist/types/tools/read.d.ts +1 -1
  39. package/dist/types/tools/search.d.ts +1 -1
  40. package/dist/types/utils/edit-mode.d.ts +69 -0
  41. package/package.json +8 -8
  42. package/scripts/generate-hotkeys-docs.ts +8 -6
  43. package/src/commands/sdk.ts +23 -2
  44. package/src/config/file-lock.ts +25 -5
  45. package/src/config/keybindings.ts +4 -2
  46. package/src/config/model-registry.ts +20 -0
  47. package/src/config/models-config-schema.ts +1 -0
  48. package/src/config/settings-schema.ts +29 -22
  49. package/src/config/settings.ts +42 -10
  50. package/src/coordinator-mcp/server.ts +87 -106
  51. package/src/edit/index.ts +5 -19
  52. package/src/edit/renderer.ts +3 -2
  53. package/src/edit/streaming.ts +28 -15
  54. package/src/eval/py/executor.ts +108 -10
  55. package/src/eval/py/kernel.ts +86 -12
  56. package/src/eval/py/runtime.ts +200 -16
  57. package/src/exec/bash-executor.ts +36 -37
  58. package/src/gjc-runtime/gc-runtime.ts +25 -3
  59. package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
  60. package/src/gjc-runtime/session-state-sidecar.ts +110 -4
  61. package/src/gjc-runtime/tmux-sessions.ts +34 -5
  62. package/src/internal-urls/docs-index.generated.ts +9 -9
  63. package/src/modes/acp/acp-agent.ts +10 -2
  64. package/src/modes/components/model-selector.ts +28 -8
  65. package/src/modes/controllers/plan-mode-controller.ts +3 -3
  66. package/src/modes/controllers/selector-controller.ts +4 -2
  67. package/src/modes/interactive-mode.ts +4 -0
  68. package/src/runtime/process-lifecycle.ts +126 -27
  69. package/src/runtime-mcp/transports/stdio.ts +11 -6
  70. package/src/sdk/acp/adapter.ts +18 -10
  71. package/src/sdk/broker/broker.ts +55 -4
  72. package/src/sdk/broker/ensure.ts +10 -7
  73. package/src/sdk/broker/identity.ts +10 -0
  74. package/src/sdk/broker/lifecycle-ledger.ts +56 -6
  75. package/src/sdk/broker/lifecycle.ts +53 -10
  76. package/src/sdk/broker/process-incarnation.ts +6 -1
  77. package/src/sdk/broker/session-index.ts +28 -11
  78. package/src/sdk/broker/startup-failure.ts +101 -0
  79. package/src/sdk/broker/transport.ts +1 -0
  80. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  81. package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
  82. package/src/sdk/bus/conversation-store.ts +171 -50
  83. package/src/sdk/bus/index.ts +101 -47
  84. package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
  85. package/src/sdk/bus/notification-service.ts +3 -3
  86. package/src/sdk/bus/reconciliation-store.ts +16 -0
  87. package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
  88. package/src/sdk/bus/telegram-daemon.ts +151 -41
  89. package/src/sdk/bus/topic-registry.ts +38 -9
  90. package/src/sdk/cli/session-cli.ts +20 -7
  91. package/src/sdk/client/discovery.ts +23 -2
  92. package/src/sdk/host/query/cursor.ts +4 -0
  93. package/src/sdk/host/query/handlers.ts +99 -100
  94. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  95. package/src/sdk/host/session-runtime.test.ts +98 -1
  96. package/src/sdk/host/session-runtime.ts +57 -15
  97. package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
  98. package/src/sdk/host/websocket-transport.ts +54 -30
  99. package/src/sdk/index.ts +1 -0
  100. package/src/sdk/mcp/server.ts +19 -6
  101. package/src/sdk/protocol/operation-inventory.generated.json +1 -1
  102. package/src/sdk/protocol/operation-registry.ts +8 -12
  103. package/src/sdk/session.ts +5 -14
  104. package/src/sdk/turn-result.ts +53 -0
  105. package/src/session/agent-session.ts +112 -69
  106. package/src/session/blob-store.ts +91 -21
  107. package/src/session/internal/managed-session-storage.ts +136 -21
  108. package/src/session/session-manager.ts +408 -81
  109. package/src/session/session-storage.ts +68 -10
  110. package/src/slash-commands/builtin-registry.ts +43 -5
  111. package/src/slash-commands/helpers/fast-status-report.ts +24 -8
  112. package/src/task/executor.ts +25 -8
  113. package/src/task/index.ts +5 -4
  114. package/src/tools/bash.ts +11 -9
  115. package/src/tools/index.ts +1 -1
  116. package/src/tools/read.ts +12 -11
  117. package/src/tools/search.ts +7 -7
  118. package/src/utils/edit-mode.ts +180 -9
  119. package/src/web/search/providers/insane.ts +55 -1
@@ -748,14 +748,47 @@ function fsyncDirectorySync(pathname: string): void {
748
748
 
749
749
  /** Best-effort identity-checked removal of one of our own staged temp names. */
750
750
  function unlinkOwnedStagedSync(stagingPath: string, expected: { dev: bigint; ino: bigint }): void {
751
+ let fd: number | undefined;
751
752
  try {
752
753
  const named = fs.lstatSync(stagingPath, { bigint: true });
753
754
  if (!named.isFile() || named.isSymbolicLink() || named.dev !== expected.dev || named.ino !== expected.ino) return;
754
- const sha256 = createHash("sha256").update(fs.readFileSync(stagingPath)).digest("hex");
755
- exactRemoveSessionStorageLockPath(stagingPath, named, sha256, named.nlink > 1n);
755
+ fd = fs.openSync(stagingPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
756
+ const opened = fs.fstatSync(fd, { bigint: true });
757
+ if (
758
+ !opened.isFile() ||
759
+ opened.dev !== named.dev ||
760
+ opened.ino !== named.ino ||
761
+ opened.nlink !== named.nlink ||
762
+ opened.size !== named.size ||
763
+ opened.mtimeNs !== named.mtimeNs ||
764
+ opened.ctimeNs !== named.ctimeNs
765
+ )
766
+ return;
767
+ const hash = createHash("sha256");
768
+ const buffer = Buffer.allocUnsafe(64 * 1024);
769
+ let offset = 0;
770
+ while (offset < Number(opened.size)) {
771
+ const length = Math.min(buffer.byteLength, Number(opened.size) - offset);
772
+ const count = fs.readSync(fd, buffer, 0, length, offset);
773
+ if (count === 0) return;
774
+ hash.update(buffer.subarray(0, count));
775
+ offset += count;
776
+ }
777
+ const after = fs.fstatSync(fd, { bigint: true });
778
+ if (
779
+ after.dev !== opened.dev ||
780
+ after.ino !== opened.ino ||
781
+ after.nlink !== opened.nlink ||
782
+ after.size !== opened.size ||
783
+ after.mtimeNs !== opened.mtimeNs ||
784
+ after.ctimeNs !== opened.ctimeNs
785
+ )
786
+ return;
787
+ exactRemoveSessionStorageLockPath(stagingPath, after, hash.digest("hex"), after.nlink > 1n);
756
788
  } catch {
757
789
  // ENOENT means the name was already consumed or removed; cleanup is best-effort.
758
- return;
790
+ } finally {
791
+ if (fd !== undefined) fs.closeSync(fd);
759
792
  }
760
793
  }
761
794
 
@@ -1601,16 +1634,16 @@ function parseSessionStorageLockOwner(bytes: Buffer): SessionStorageLockOwner |
1601
1634
  }
1602
1635
  }
1603
1636
 
1604
- function exactRemoveSessionStorageLockPath(
1637
+ function exactRemoveSessionStorageLockPathResult(
1605
1638
  lockPath: string,
1606
1639
  expected: fs.BigIntStats,
1607
1640
  sha256: string,
1608
1641
  allowHardLink = false,
1609
- ): boolean {
1642
+ ): native.NativeExactUnlinkResult {
1610
1643
  try {
1611
1644
  const parent = fs.lstatSync(path.dirname(lockPath), { bigint: true });
1612
- if (!parent.isDirectory() || parent.isSymbolicLink()) return false;
1613
- const result = nativeSessionStorage().exactUnlink(lockPath, {
1645
+ if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
1646
+ return nativeSessionStorage().exactUnlink(lockPath, {
1614
1647
  dev: expected.dev,
1615
1648
  ino: expected.ino,
1616
1649
  nlink: expected.nlink,
@@ -1622,12 +1655,27 @@ function exactRemoveSessionStorageLockPath(
1622
1655
  quarantineName: `${path.basename(lockPath)}.reap-${randomUUID()}`,
1623
1656
  allowHardLink,
1624
1657
  });
1625
- return result.ok || (result.code === "cleanup_pending" && typeof result.detachedPath === "string");
1626
1658
  } catch {
1627
- return false;
1659
+ return { ok: false, code: "native_failure" };
1628
1660
  }
1629
1661
  }
1630
1662
 
1663
+ function exactRemoveSessionStorageLockPath(
1664
+ lockPath: string,
1665
+ expected: fs.BigIntStats,
1666
+ sha256: string,
1667
+ allowHardLink = false,
1668
+ ): boolean {
1669
+ const result = exactRemoveSessionStorageLockPathResult(lockPath, expected, sha256, allowHardLink);
1670
+ return (
1671
+ result.ok ||
1672
+ (result.code === "cleanup_pending" &&
1673
+ result.payloadDurable === true &&
1674
+ !result.retainedSuccessorPath &&
1675
+ !result.retainedUnknownPath)
1676
+ );
1677
+ }
1678
+
1631
1679
  function exactRemoveOwnedSessionStorageLockPath(
1632
1680
  lockPath: string,
1633
1681
  expected: Pick<fs.BigIntStats, "dev" | "ino">,
@@ -1836,7 +1884,17 @@ export class FileSessionStorage implements SessionStorage {
1836
1884
  return {
1837
1885
  releaseSync: () => {
1838
1886
  if (released) return;
1839
- if (!exactRemoveSessionStorageLockPath(lockPath, expected, ownerDigest!)) {
1887
+ const removal = exactRemoveSessionStorageLockPathResult(lockPath, expected, ownerDigest!);
1888
+ if (!removal.ok) {
1889
+ if (
1890
+ removal.code === "cleanup_pending" &&
1891
+ removal.payloadDurable === true &&
1892
+ !removal.retainedSuccessorPath &&
1893
+ !removal.retainedUnknownPath
1894
+ ) {
1895
+ released = true;
1896
+ return;
1897
+ }
1840
1898
  try {
1841
1899
  const current = fs.lstatSync(lockPath, { bigint: true });
1842
1900
  if (current.dev === expected.dev && current.ino === expected.ino) {
@@ -23,7 +23,7 @@ import {
23
23
  } from "../config/model-resolver";
24
24
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../discovery/helpers.js";
25
25
  import { DynamicBorder } from "../modes/components/dynamic-border";
26
- import { theme } from "../modes/theme/theme";
26
+ import { getAvailableThemes, getDetectedThemeSettingsPath, setTheme, theme } from "../modes/theme/theme";
27
27
  import {
28
28
  type ComposerSubmissionOptions,
29
29
  canApplyComposerSubmission,
@@ -709,10 +709,48 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
709
709
  },
710
710
  {
711
711
  name: "theme",
712
- description: "Open theme selector",
713
- handleTui: (_command, runtime) => {
714
- runtime.ctx.showThemeSelector();
715
- runtime.ctx.editor.setText("");
712
+ description: "Change theme immediately, or open the theme selector without args",
713
+ inlineHint: "[theme]",
714
+ allowArgs: true,
715
+ handleTui: async (command, runtime) => {
716
+ const ctx = runtime.ctx;
717
+ const name = command.args?.trim();
718
+ if (!name) {
719
+ ctx.showThemeSelector();
720
+ ctx.editor.setText("");
721
+ return;
722
+ }
723
+ const available = await getAvailableThemes();
724
+ if (!available.includes(name)) {
725
+ ctx.showError(`Unknown theme "${name}". Available themes: ${available.join(", ")}`);
726
+ ctx.editor.setText("");
727
+ return;
728
+ }
729
+ if (!ctx.settings.canWriteDurableConfig()) {
730
+ ctx.showError(
731
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
732
+ );
733
+ ctx.editor.setText("");
734
+ return;
735
+ }
736
+ try {
737
+ ctx.settings.set(getDetectedThemeSettingsPath(), name);
738
+ } catch (error) {
739
+ ctx.showError(error instanceof Error ? error.message : String(error));
740
+ ctx.editor.setText("");
741
+ return;
742
+ }
743
+ const result = await setTheme(name, true, { shouldApply: () => !ctx.isStopped?.() });
744
+ if (ctx.isStopped?.()) return;
745
+ ctx.statusLine.invalidate();
746
+ ctx.updateEditorTopBorder();
747
+ ctx.ui.invalidate();
748
+ if (result.success) {
749
+ ctx.showStatus(`Theme changed to ${name}`);
750
+ } else {
751
+ ctx.showError(`Failed to load theme "${name}": ${result.error}\nFell back to dark theme.`);
752
+ }
753
+ ctx.editor.setText("");
716
754
  },
717
755
  },
718
756
  {
@@ -1,4 +1,4 @@
1
- import type { Model } from "@gajae-code/ai/core";
1
+ import { type Model, modelSupportsServiceTier, modelsAreEqual } from "@gajae-code/ai/core";
2
2
 
3
3
  /**
4
4
  * A single line in the `/fast status` report: a labelled model and whether fast
@@ -55,7 +55,7 @@ export function formatFastStatusReport(args: FormatFastStatusReportArgs): string
55
55
  export interface FastStatusSessionLike {
56
56
  readonly model?: Model;
57
57
  /** Fast predicate against the main session tier (current model + `modelRoles`). */
58
- isFastForProvider(provider?: string): boolean;
58
+ isFastForProvider(provider?: string, supportsServiceTier?: boolean): boolean;
59
59
  /**
60
60
  * Current-model EFFECTIVE fast state (intent minus any provider auto-disable).
61
61
  * Used for the current-model row so it matches what the next request does.
@@ -63,8 +63,13 @@ export interface FastStatusSessionLike {
63
63
  */
64
64
  isFastModeActive?(): boolean;
65
65
  /** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
66
- isFastForSubagentProvider(provider?: string): boolean;
66
+ isFastForSubagentProvider(provider?: string, supportsServiceTier?: boolean): boolean;
67
67
  resolveRoleModelWithThinking(role: string): { model?: Model };
68
+ /** Runtime fallback position for the configured DEFAULT chain. */
69
+ getDefaultFallbackRuntimeState?(): {
70
+ chain: { entries: readonly string[] };
71
+ controller: { activeIndex: number };
72
+ };
68
73
  }
69
74
 
70
75
  /** A role to enumerate in the report, with the tier source its subagent runs under. */
@@ -106,15 +111,26 @@ export function buildFastStatusReport(args: BuildFastStatusReportArgs): string {
106
111
  // on pure intent. Fall back to intent when a fake omits `isFastModeActive`.
107
112
  const currentFast = session.isFastModeActive
108
113
  ? session.isFastModeActive()
109
- : session.isFastForProvider(session.model?.provider);
114
+ : session.isFastForProvider(session.model?.provider, modelSupportsServiceTier(session.model));
110
115
  const rows: FastStatusRow[] = [{ label: "현재 모델", model: session.model, fast: currentFast }];
111
116
  for (const target of roleTargets) {
112
117
  const resolved = session.resolveRoleModelWithThinking(target.id);
113
- if (resolved.model) {
118
+ const fallbackState = target.id === "default" ? session.getDefaultFallbackRuntimeState?.() : undefined;
119
+ const activeDefaultFallback =
120
+ fallbackState !== undefined &&
121
+ fallbackState.chain.entries.length > 1 &&
122
+ fallbackState.controller.activeIndex > 0 &&
123
+ fallbackState.controller.activeIndex < fallbackState.chain.entries.length;
124
+ const rowModel = activeDefaultFallback && session.model ? session.model : resolved.model;
125
+ if (rowModel) {
126
+ const supportsServiceTier = modelSupportsServiceTier(rowModel);
127
+ const isCurrentModel = session.model !== undefined && modelsAreEqual(session.model, rowModel);
114
128
  const fast = target.isSubagentRole
115
- ? session.isFastForSubagentProvider(resolved.model.provider)
116
- : session.isFastForProvider(resolved.model.provider);
117
- rows.push({ label: target.label, model: resolved.model, fast });
129
+ ? session.isFastForSubagentProvider(rowModel.provider, supportsServiceTier)
130
+ : isCurrentModel
131
+ ? currentFast
132
+ : session.isFastForProvider(rowModel.provider, supportsServiceTier);
133
+ rows.push({ label: target.label, model: rowModel, fast });
118
134
  }
119
135
  }
120
136
  return formatFastStatusReport({ rows, iconFast, formatInactive });
@@ -15,7 +15,14 @@ import type {
15
15
  } from "@gajae-code/agent-core";
16
16
  import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
17
17
  import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
18
- import type { AssistantMessage, Message, Model, ServiceTier } from "@gajae-code/ai/core";
18
+ import {
19
+ type AssistantMessage,
20
+ isFastModeEffectiveForProvider,
21
+ type Message,
22
+ type Model,
23
+ modelSupportsServiceTier,
24
+ type ServiceTier,
25
+ } from "@gajae-code/ai/core";
19
26
  import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-code/ai/utils/schema";
20
27
  import { logger, prompt, untilAborted } from "@gajae-code/utils";
21
28
  import { AsyncJobManager } from "../async";
@@ -248,14 +255,11 @@ export interface ExecutorOptions {
248
255
  /** Parent session's registry; shared by child sessions for IRC routing and roster visibility. */
249
256
  agentRegistry?: AgentRegistry;
250
257
  /**
251
- * Live service-tier intent of the parent session (`AgentSession.serviceTier`),
252
- * used as the inherited tier when `task.serviceTier === "inherit"`. Passing the
253
- * live value (not the stale settings snapshot) lets a runtime `/fast on` reach
254
- * subagents, and a main-model fast-mode auto-disable does not clobber it.
258
+ * Parent service-tier intent captured when the child is spawned. Used when
259
+ * `task.serviceTier === "inherit"` so request serialization and reporting use
260
+ * the same immutable child settings snapshot.
255
261
  */
256
262
  inheritedServiceTier?: ServiceTier;
257
- /** Resolve whether the effective subagent tier grants fast mode for the selected provider. */
258
- isFastForSubagentProvider?: (provider?: string) => boolean;
259
263
  /** Override local:// protocol options so subagent shares parent's local:// root */
260
264
  localProtocolOptions?: LocalProtocolOptions;
261
265
  /**
@@ -865,6 +869,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
865
869
 
866
870
  const settings = options.settings ?? Settings.isolated();
867
871
  const subagentSettings = createSubagentSettings(settings, options.inheritedServiceTier);
872
+ const configuredSubagentServiceTier = subagentSettings.get("serviceTier");
873
+ const subagentServiceTier = configuredSubagentServiceTier === "none" ? undefined : configuredSubagentServiceTier;
874
+ const isFastForModel = (candidate: Model | undefined): boolean =>
875
+ candidate !== undefined &&
876
+ isFastModeEffectiveForProvider(subagentServiceTier, candidate.provider, modelSupportsServiceTier(candidate));
868
877
  const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
869
878
  const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
870
879
  const parentDepth = options.taskDepth ?? 0;
@@ -1533,7 +1542,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1533
1542
  resolvedModelString = formatModelString(model);
1534
1543
  activeProviderModelString = resolvedModelString;
1535
1544
  }
1536
- progress.fastMode = model ? (options.isFastForSubagentProvider?.(model.provider) ?? false) : false;
1545
+ progress.fastMode = isFastForModel(model);
1537
1546
  if (authFallbackUsed && model && requestedModel) {
1538
1547
  modelSubstitutionWarning = {
1539
1548
  requested: formatModelString(requestedModel),
@@ -1954,6 +1963,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1954
1963
  }
1955
1964
  if (event.type === "model_fallback_switched") {
1956
1965
  activeProviderModelString = event.to;
1966
+ progress.fastMode = isFastForModel(session.model);
1967
+ AsyncJobManager.instance()?.updateSubagentModel?.(options.subagentId ?? id, {
1968
+ requestedModel: modelSubstitutionWarning?.requested ?? resolvedModelString,
1969
+ effectiveModel: event.to,
1970
+ modelFellBack: true,
1971
+ fastMode: progress.fastMode,
1972
+ });
1973
+ scheduleProgress(true);
1957
1974
  forwardSubagentEvent(event);
1958
1975
  return;
1959
1976
  }
package/src/task/index.ts CHANGED
@@ -1900,6 +1900,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1900
1900
  file => path.basename(file.path).toLowerCase() !== "agents.md",
1901
1901
  );
1902
1902
  const promptTemplates = this.session.promptTemplates;
1903
+ const parentMcpManager = this.session.getMcpManager?.();
1904
+ // Exact-config tools-only managers belong to the top-level ACP session and cannot be reused by sub-sessions.
1905
+ const reusableParentMcpManager = parentMcpManager?.isToolsOnly() ? undefined : parentMcpManager;
1903
1906
 
1904
1907
  // Initialize progress for all tasks
1905
1908
  for (let i = 0; i < tasksWithUniqueIds.length; i++) {
@@ -2027,7 +2030,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2027
2030
  agentRegistry: this.session.agentRegistry,
2028
2031
  settings: this.session.settings,
2029
2032
  inheritedServiceTier: this.session.serviceTier,
2030
- isFastForSubagentProvider: provider => this.session.isFastForSubagentProvider?.(provider) ?? false,
2031
2033
  contextFiles,
2032
2034
  skills: availableSkills,
2033
2035
  autoloadSkills: resolvedAutoloadSkills,
@@ -2037,7 +2039,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2037
2039
  parentArtifactManager,
2038
2040
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
2039
2041
  parentTelemetry: this.session.getTelemetry?.(),
2040
- parentMcpManager: this.session.getMcpManager?.(),
2042
+ parentMcpManager: reusableParentMcpManager,
2041
2043
  forkContextSeed,
2042
2044
  });
2043
2045
  return {
@@ -2105,7 +2107,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2105
2107
  agentRegistry: this.session.agentRegistry,
2106
2108
  settings: this.session.settings,
2107
2109
  inheritedServiceTier: this.session.serviceTier,
2108
- isFastForSubagentProvider: provider => this.session.isFastForSubagentProvider?.(provider) ?? false,
2109
2110
  contextFiles,
2110
2111
  skills: availableSkills,
2111
2112
  autoloadSkills: resolvedAutoloadSkills,
@@ -2115,7 +2116,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2115
2116
  parentArtifactManager,
2116
2117
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
2117
2118
  parentTelemetry: this.session.getTelemetry?.(),
2118
- parentMcpManager: this.session.getMcpManager?.(),
2119
+ parentMcpManager: reusableParentMcpManager,
2119
2120
  forkContextSeed,
2120
2121
  });
2121
2122
  let capturedResult = {
package/src/tools/bash.ts CHANGED
@@ -862,26 +862,28 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
862
862
  return { kind: "aborted" };
863
863
  }
864
864
 
865
+ const threshold = Promise.withResolvers<{ kind: "running" }>();
866
+ const thresholdTimer = setTimeout(() => threshold.resolve({ kind: "running" }), Math.max(0, thresholdMs));
865
867
  const waiters: Array<Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "aborted" }>> = [
866
868
  job.completion,
867
- Bun.sleep(thresholdMs).then(() => ({ kind: "running" as const })),
869
+ threshold.promise,
868
870
  ];
869
871
  if (backgroundRequest) {
870
872
  waiters.push(backgroundRequest.then(() => ({ kind: "running" as const })));
871
873
  }
872
874
 
873
- if (!signal) {
874
- return await Promise.race(waiters);
875
+ let onAbort: (() => void) | undefined;
876
+ if (signal) {
877
+ const aborted = Promise.withResolvers<{ kind: "aborted" }>();
878
+ onAbort = () => aborted.resolve({ kind: "aborted" });
879
+ signal.addEventListener("abort", onAbort, { once: true });
880
+ waiters.push(aborted.promise);
875
881
  }
876
-
877
- const { promise: abortedPromise, resolve: resolveAborted } = Promise.withResolvers<{ kind: "aborted" }>();
878
- const onAbort = () => resolveAborted({ kind: "aborted" });
879
- signal.addEventListener("abort", onAbort, { once: true });
880
- waiters.push(abortedPromise);
881
882
  try {
882
883
  return await Promise.race(waiters);
883
884
  } finally {
884
- signal.removeEventListener("abort", onAbort);
885
+ clearTimeout(thresholdTimer);
886
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
885
887
  }
886
888
  }
887
889
 
@@ -282,7 +282,7 @@ export interface ToolSession {
282
282
  /** Live service-tier intent of the parent session, inherited by `inherit` subagents. */
283
283
  serviceTier?: ServiceTier;
284
284
  /** Whether the effective subagent tier grants fast mode for a resolved provider. */
285
- isFastForSubagentProvider?: (provider?: string) => boolean;
285
+ isFastForSubagentProvider?: (provider?: string, supportsServiceTier?: boolean) => boolean;
286
286
  /** Plan mode state (if active) */
287
287
  getPlanModeState?: () => PlanModeState | undefined;
288
288
  /** Goal mode state (if active or paused) */
package/src/tools/read.ts CHANGED
@@ -1447,7 +1447,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1447
1447
  readonly name = "read";
1448
1448
  readonly label = "Read";
1449
1449
  readonly loadMode = "essential";
1450
- readonly description: string;
1450
+ get description(): string {
1451
+ const displayMode = resolveFileDisplayMode(this.session);
1452
+ return prompt.render(readDescription, {
1453
+ DEFAULT_LIMIT: String(this.#defaultLimit),
1454
+ DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
1455
+ TRUNCATION_DEFAULT: String(this.session.settings.get("read.truncation") ?? "last"),
1456
+ RECEIPT_LINES: String(this.session.settings.get("read.receiptBudgetLines")),
1457
+ RECEIPT_KIB: String(this.session.settings.get("read.receiptBudgetBytes")),
1458
+ IS_HL_MODE: displayMode.hashLines,
1459
+ IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
1460
+ });
1461
+ }
1451
1462
  readonly parameters = readSchema;
1452
1463
  readonly nonAbortable = true;
1453
1464
  readonly strict = true;
@@ -1457,21 +1468,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1457
1468
  readonly #defaultLimit: number;
1458
1469
 
1459
1470
  constructor(private readonly session: ToolSession) {
1460
- const displayMode = resolveFileDisplayMode(session);
1461
1471
  this.#autoResizeImages = session.settings.get("images.autoResize");
1462
1472
  this.#defaultLimit = Math.max(
1463
1473
  1,
1464
1474
  Math.min(session.settings.get("read.defaultLimit") ?? DEFAULT_MAX_LINES, DEFAULT_MAX_LINES),
1465
1475
  );
1466
- this.description = prompt.render(readDescription, {
1467
- DEFAULT_LIMIT: String(this.#defaultLimit),
1468
- DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
1469
- TRUNCATION_DEFAULT: String(this.session.settings.get("read.truncation") ?? "last"),
1470
- RECEIPT_LINES: String(this.session.settings.get("read.receiptBudgetLines")),
1471
- RECEIPT_KIB: String(this.session.settings.get("read.receiptBudgetBytes")),
1472
- IS_HL_MODE: displayMode.hashLines,
1473
- IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
1474
- });
1475
1476
  }
1476
1477
 
1477
1478
  async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
@@ -237,17 +237,17 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
237
237
  readonly label = "Search";
238
238
  readonly loadMode = "discoverable";
239
239
  readonly summary = "Search file contents using ripgrep (fast text search)";
240
- readonly description: string;
241
- readonly parameters = searchSchema;
242
- readonly strict = true;
243
-
244
- constructor(private readonly session: ToolSession) {
245
- const displayMode = resolveFileDisplayMode(session);
246
- this.description = prompt.render(searchDescription, {
240
+ get description(): string {
241
+ const displayMode = resolveFileDisplayMode(this.session);
242
+ return prompt.render(searchDescription, {
247
243
  IS_HL_MODE: displayMode.hashLines,
248
244
  IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
249
245
  });
250
246
  }
247
+ readonly parameters = searchSchema;
248
+ readonly strict = true;
249
+
250
+ constructor(private readonly session: ToolSession) {}
251
251
 
252
252
  async execute(
253
253
  _toolCallId: string,