@gajae-code/coding-agent 0.17.6 → 0.17.7

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 (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/cli/public-command-errors.d.ts +2 -0
  3. package/dist/types/commands/sdk.d.ts +7 -1
  4. package/dist/types/config/model-profile-ownership.d.ts +74 -0
  5. package/dist/types/config/model-registry.d.ts +2 -1
  6. package/dist/types/config/settings-schema.d.ts +32 -0
  7. package/dist/types/sdk/broker/lifecycle.d.ts +4 -1
  8. package/dist/types/sdk/lifecycle/service.d.ts +2 -0
  9. package/dist/types/sdk/lifecycle-session.d.ts +5 -1
  10. package/dist/types/sdk/session-directory.d.ts +1 -0
  11. package/dist/types/sdk/session-list.d.ts +11 -0
  12. package/dist/types/sdk/startup-capability.d.ts +1 -1
  13. package/dist/types/session/agent-session.d.ts +6 -0
  14. package/dist/types/session/internal/managed-session-scope.d.ts +4 -0
  15. package/dist/types/session/session-manager.d.ts +6 -0
  16. package/dist/types/setup/model-onboarding-guidance.d.ts +18 -0
  17. package/dist/types/task/delegation-hint.d.ts +53 -0
  18. package/examples/README.md +3 -3
  19. package/examples/custom-tools/README.md +1 -1
  20. package/examples/hooks/README.md +1 -1
  21. package/package.json +7 -7
  22. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  23. package/src/cli/config-cli.ts +2 -0
  24. package/src/cli/public-command-errors.ts +6 -0
  25. package/src/commands/sdk.ts +361 -117
  26. package/src/config/model-profile-ownership.ts +303 -0
  27. package/src/config/model-registry.ts +53 -14
  28. package/src/config/settings-schema.ts +81 -0
  29. package/src/internal-urls/docs-index.generated.ts +18 -17
  30. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  31. package/src/modes/controllers/selector-controller.ts +3 -29
  32. package/src/sdk/broker/broker.ts +120 -23
  33. package/src/sdk/broker/lifecycle.ts +89 -13
  34. package/src/sdk/cli/session-cli.ts +45 -9
  35. package/src/sdk/host/control/dispatch.ts +6 -0
  36. package/src/sdk/lifecycle/service.ts +186 -3
  37. package/src/sdk/lifecycle-session.ts +35 -8
  38. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  39. package/src/sdk/protocol/operation-registry.ts +5 -1
  40. package/src/sdk/session-directory.ts +1 -1
  41. package/src/sdk/session-list.ts +47 -0
  42. package/src/sdk/session.ts +290 -114
  43. package/src/sdk/startup-capability.ts +2 -1
  44. package/src/session/agent-session.ts +102 -4
  45. package/src/session/internal/managed-session-scope.ts +81 -17
  46. package/src/session/session-manager.ts +70 -11
  47. package/src/setup/model-onboarding-guidance.ts +26 -0
  48. package/src/task/delegation-hint.ts +578 -0
@@ -236,6 +236,9 @@ import {
236
236
 
237
237
  export type { AutomationToolName, AutomationTools } from "../tools";
238
238
 
239
+ const MAX_STARTUP_CREDENTIAL_DISABLED_EVENT_BUFFER = 32;
240
+ const startupCredentialDisabledEventsByAuthStorage = new WeakMap<AuthStorage, CredentialDisabledEvent[]>();
241
+
239
242
  type AsyncResultEntry = {
240
243
  jobId: string;
241
244
  generation: string;
@@ -753,17 +756,7 @@ export async function discoverAuthStorage(
753
756
  sourceLabel: `broker ${brokerConfig.url}`,
754
757
  credentialRankingMode,
755
758
  });
756
- try {
757
- await storage.reload();
758
- } catch (error) {
759
- try {
760
- storage.close();
761
- } catch {
762
- // Preserve the initial reload failure.
763
- }
764
- throw error;
765
- }
766
- return storage;
759
+ return await reloadDiscoveredAuthStorage(storage);
767
760
  }
768
761
  const dbPath = getAgentDbPath(agentDir);
769
762
  const storage = await AuthStorage.create(dbPath, {
@@ -771,13 +764,17 @@ export async function discoverAuthStorage(
771
764
  sourceLabel: `local ${dbPath}`,
772
765
  credentialRankingMode,
773
766
  });
767
+ return await reloadDiscoveredAuthStorage(storage);
768
+ }
769
+
770
+ async function reloadDiscoveredAuthStorage(storage: AuthStorage): Promise<AuthStorage> {
774
771
  try {
775
772
  await storage.reload();
776
773
  } catch (error) {
777
774
  try {
778
775
  storage.close();
779
- } catch {
780
- // Preserve the initial reload failure.
776
+ } catch (cleanupError) {
777
+ throw attachStartupCleanupDiagnostic(error, cleanupError);
781
778
  }
782
779
  throw error;
783
780
  }
@@ -1396,6 +1393,31 @@ function attachMcpCleanupDiagnostic(primary: unknown, cleanup: unknown): unknown
1396
1393
  return new McpManagerCleanupDiagnosticError(primary, cleanup);
1397
1394
  }
1398
1395
 
1396
+ function attachStartupCleanupDiagnostic(primary: unknown, cleanup: unknown): unknown {
1397
+ const diagnostic = { code: "STARTUP_RESOURCE_CLEANUP_FAILED" as const, cause: cleanup };
1398
+ if (primary && (typeof primary === "object" || typeof primary === "function")) {
1399
+ try {
1400
+ Object.defineProperty(primary, "startupCleanupDiagnostic", {
1401
+ value: diagnostic,
1402
+ enumerable: false,
1403
+ configurable: true,
1404
+ });
1405
+ if ((primary as { startupCleanupDiagnostic?: unknown }).startupCleanupDiagnostic === diagnostic)
1406
+ if ((primary as { startupCleanupDiagnostic?: unknown }).startupCleanupDiagnostic === diagnostic)
1407
+ return primary;
1408
+ } catch {
1409
+ // Frozen or hostile proxy errors retain their original message in this typed wrapper.
1410
+ }
1411
+ }
1412
+ const wrapped = new AggregateError([primary, cleanup], safeErrorDescription(primary), { cause: primary });
1413
+ Object.defineProperty(wrapped, "startupCleanupDiagnostic", {
1414
+ value: diagnostic,
1415
+ enumerable: false,
1416
+ configurable: true,
1417
+ });
1418
+ return wrapped;
1419
+ }
1420
+
1399
1421
  function findExactMcpToolNameCollisions(
1400
1422
  exactMcpToolNames: readonly string[],
1401
1423
  catalogToolNames: Iterable<string>,
@@ -1540,29 +1562,161 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1540
1562
  // / session would silently miss credential_disabled events.
1541
1563
  // Injected auth is the caller's authority. Only the CLI root can hand off its
1542
1564
  // explicit startup snapshot; never discover global config for injected SDK auth.
1543
- const startupAuthConfig = hasInjectedAuth
1544
- ? options.modelRegistryStartupMutation?.owner === "cli-root"
1545
- ? options.startupAuthConfig
1546
- : undefined
1547
- : (options.startupAuthConfig ?? (await resolveStartupAuthConfig(agentDir)));
1548
1565
  const ownsModelRegistry = options.modelRegistry === undefined;
1549
1566
  const ownsAuthStorage = options.modelRegistry === undefined && options.authStorage === undefined;
1550
- const modelRegistry =
1551
- options.modelRegistry ??
1552
- new ModelRegistry(
1553
- options.authStorage ??
1554
- (await logger.time("discoverModels", () => discoverAuthStorage(agentDir, startupAuthConfig))),
1555
- path.join(agentDir, "models.yml"),
1556
- undefined,
1557
- { agentDir },
1558
- );
1559
- const authStorage = modelRegistry.authStorage;
1560
- const authStorageOwner = modelRegistry.getAuthStorageOwner();
1561
- if (options.authStorage && options.authStorage !== authStorage) {
1567
+ const ownsScopedSettings = options.settings === undefined;
1568
+ if (options.modelRegistry && options.authStorage && options.authStorage !== options.modelRegistry.authStorage) {
1562
1569
  throw new Error(
1563
1570
  "options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
1564
1571
  );
1565
1572
  }
1573
+ const settingsPromise =
1574
+ options.settings === undefined
1575
+ ? logger.time("settings", Settings.loadForScope, { cwd, agentDir })
1576
+ : Promise.resolve(options.settings);
1577
+ type StartupValue<T> = { ok: true; value: T } | { ok: false; error: unknown };
1578
+ const captureStartupValue = <T>(pending: Promise<T>): Promise<StartupValue<T>> =>
1579
+ pending.then(
1580
+ value => ({ ok: true as const, value }),
1581
+ error => ({ ok: false as const, error }),
1582
+ );
1583
+ const settingsOutcome = captureStartupValue(settingsPromise);
1584
+ const startupAuthConfigPromise = Promise.resolve(
1585
+ hasInjectedAuth
1586
+ ? options.modelRegistryStartupMutation?.owner === "cli-root"
1587
+ ? options.startupAuthConfig
1588
+ : undefined
1589
+ : (options.startupAuthConfig ?? resolveStartupAuthConfig(agentDir)),
1590
+ );
1591
+ const startupAuthConfigOutcome = captureStartupValue(startupAuthConfigPromise);
1592
+ const authStoragePromise = options.modelRegistry
1593
+ ? Promise.resolve(options.modelRegistry.authStorage)
1594
+ : options.authStorage
1595
+ ? Promise.resolve(options.authStorage)
1596
+ : startupAuthConfigPromise.then(startupAuthConfig =>
1597
+ logger.time("discoverModels", () => discoverAuthStorage(agentDir, startupAuthConfig)),
1598
+ );
1599
+ const authStorageOutcome = captureStartupValue(authStoragePromise);
1600
+ const discoveredAuthStorage = await authStorageOutcome;
1601
+ if (!discoveredAuthStorage.ok) {
1602
+ const loadedSettings = await settingsOutcome;
1603
+ const cleanupErrors: unknown[] = [];
1604
+ if (loadedSettings.ok && ownsScopedSettings) {
1605
+ try {
1606
+ await loadedSettings.value.close();
1607
+ } catch (cleanupError) {
1608
+ cleanupErrors.push(cleanupError);
1609
+ }
1610
+ try {
1611
+ releaseSettingsScope(loadedSettings.value);
1612
+ } catch (cleanupError) {
1613
+ cleanupErrors.push(cleanupError);
1614
+ }
1615
+ }
1616
+ if (cleanupErrors.length > 0)
1617
+ throw attachStartupCleanupDiagnostic(
1618
+ discoveredAuthStorage.error,
1619
+ new AggregateError(cleanupErrors, "Failed to release scoped settings after auth storage setup failure."),
1620
+ );
1621
+ throw discoveredAuthStorage.error;
1622
+ }
1623
+ const authStorage = discoveredAuthStorage.value;
1624
+ const startupCredentialDisabledEvents = startupCredentialDisabledEventsByAuthStorage.get(authStorage) ?? [];
1625
+ startupCredentialDisabledEventsByAuthStorage.delete(authStorage);
1626
+ const preserveCredentialDisabledEventsForRetry = (): void => {
1627
+ if (ownsAuthStorage || startupCredentialDisabledEvents.length === 0) return;
1628
+ const pending = startupCredentialDisabledEventsByAuthStorage.get(authStorage) ?? [];
1629
+ pending.push(...startupCredentialDisabledEvents.splice(0));
1630
+ if (pending.length > MAX_STARTUP_CREDENTIAL_DISABLED_EVENT_BUFFER)
1631
+ pending.splice(0, pending.length - MAX_STARTUP_CREDENTIAL_DISABLED_EVENT_BUFFER);
1632
+ startupCredentialDisabledEventsByAuthStorage.set(authStorage, pending);
1633
+ };
1634
+ let credentialDisabledTarget: ExtensionRunner | undefined;
1635
+ let unsubscribeCredentialDisabled: (() => void) | undefined;
1636
+ const releaseCredentialDisabledSubscription = (): void => {
1637
+ const unsubscribe = unsubscribeCredentialDisabled;
1638
+ unsubscribeCredentialDisabled = undefined;
1639
+ unsubscribe?.();
1640
+ };
1641
+ const failInitialSetup = async (primary: unknown, scopedSettings?: Settings): Promise<never> => {
1642
+ const cleanupErrors: unknown[] = [];
1643
+ preserveCredentialDisabledEventsForRetry();
1644
+ try {
1645
+ releaseCredentialDisabledSubscription();
1646
+ } catch (cleanupError) {
1647
+ cleanupErrors.push(cleanupError);
1648
+ }
1649
+ if (scopedSettings && ownsScopedSettings) {
1650
+ try {
1651
+ await scopedSettings.close();
1652
+ } catch (cleanupError) {
1653
+ cleanupErrors.push(cleanupError);
1654
+ }
1655
+ try {
1656
+ releaseSettingsScope(scopedSettings);
1657
+ } catch (cleanupError) {
1658
+ cleanupErrors.push(cleanupError);
1659
+ }
1660
+ }
1661
+ if (ownsAuthStorage) {
1662
+ try {
1663
+ authStorage.close();
1664
+ } catch (cleanupError) {
1665
+ cleanupErrors.push(cleanupError);
1666
+ }
1667
+ }
1668
+ if (cleanupErrors.length > 0)
1669
+ throw attachStartupCleanupDiagnostic(
1670
+ primary,
1671
+ new AggregateError(cleanupErrors, "Failed to release resources after initial session setup failure."),
1672
+ );
1673
+ throw primary;
1674
+ };
1675
+ // Subscribe before owned-registry construction as its first catalog pass may
1676
+ // probe credentials. Preserve the listener cleanup contract if scoped settings
1677
+ // loading fails after the storage has been acquired.
1678
+ try {
1679
+ unsubscribeCredentialDisabled = authStorage.onCredentialDisabled(event => {
1680
+ if (credentialDisabledTarget) {
1681
+ void credentialDisabledTarget.emitCredentialDisabled(event);
1682
+ } else {
1683
+ startupCredentialDisabledEvents.push(event);
1684
+ }
1685
+ });
1686
+ } catch (error) {
1687
+ const settingsAfterSubscriptionFailure = await settingsOutcome;
1688
+ if (!settingsAfterSubscriptionFailure.ok)
1689
+ return await failInitialSetup(
1690
+ new AggregateError(
1691
+ [error, settingsAfterSubscriptionFailure.error],
1692
+ "Credential listener registration and scoped settings initialization both failed.",
1693
+ ),
1694
+ );
1695
+ return await failInitialSetup(error, settingsAfterSubscriptionFailure.value);
1696
+ }
1697
+ const settingsResult = await settingsOutcome;
1698
+ if (!settingsResult.ok) return await failInitialSetup(settingsResult.error);
1699
+ const settings = settingsResult.value;
1700
+ const closeOwnedSettings = async (): Promise<void> => {
1701
+ if (!ownsScopedSettings) return;
1702
+ try {
1703
+ await settings.close();
1704
+ } finally {
1705
+ releaseSettingsScope(settings);
1706
+ }
1707
+ };
1708
+ const startupAuthConfigResult = await startupAuthConfigOutcome;
1709
+ if (!startupAuthConfigResult.ok) return await failInitialSetup(startupAuthConfigResult.error, settings);
1710
+ const startupAuthConfig = startupAuthConfigResult.value;
1711
+ let modelRegistry: ModelRegistry;
1712
+ try {
1713
+ modelRegistry =
1714
+ options.modelRegistry ??
1715
+ new ModelRegistry(authStorage, path.join(agentDir, "models.yml"), settings, { agentDir });
1716
+ } catch (error) {
1717
+ return await failInitialSetup(error, settings);
1718
+ }
1719
+ const authStorageOwner = modelRegistry.getAuthStorageOwner();
1566
1720
 
1567
1721
  let agent: Agent;
1568
1722
  let sessionAgent: Agent | undefined;
@@ -1622,22 +1776,34 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1622
1776
  let authStorageClosed = false;
1623
1777
  let credentialScopeId: string | undefined;
1624
1778
  let credentialScopeLeased = false;
1625
- let closeOwnedSettings: () => Promise<void> = async () => {};
1626
1779
  const closeOwnedAuthStorage = async (): Promise<void> => {
1627
- if (ownsModelRegistry) await modelRegistry.dispose();
1780
+ const cleanupErrors: unknown[] = [];
1781
+ if (ownsModelRegistry) {
1782
+ try {
1783
+ await modelRegistry.dispose();
1784
+ } catch (error) {
1785
+ cleanupErrors.push(error);
1786
+ }
1787
+ }
1628
1788
  if (!hasSession && credentialScopeLeased && credentialScopeId) {
1629
- authStorage.releaseCredentialScope(credentialScopeId);
1630
- credentialScopeLeased = false;
1789
+ try {
1790
+ authStorage.releaseCredentialScope(credentialScopeId);
1791
+ credentialScopeLeased = false;
1792
+ } catch (error) {
1793
+ cleanupErrors.push(error);
1794
+ }
1631
1795
  }
1632
- if (!ownsAuthStorage || authStorageClosed) return;
1633
- authStorageClosed = true;
1634
- authStorage.close();
1635
- };
1636
- let unsubscribeCredentialDisabled: (() => void) | undefined;
1637
- const releaseCredentialDisabledSubscription = (): void => {
1638
- const unsubscribe = unsubscribeCredentialDisabled;
1639
- unsubscribeCredentialDisabled = undefined;
1640
- unsubscribe?.();
1796
+ if (ownsAuthStorage && !authStorageClosed) {
1797
+ authStorageClosed = true;
1798
+ try {
1799
+ authStorage.close();
1800
+ } catch (error) {
1801
+ cleanupErrors.push(error);
1802
+ }
1803
+ }
1804
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
1805
+ if (cleanupErrors.length > 1)
1806
+ throw new AggregateError(cleanupErrors, "Failed to close owned model registry and auth storage.");
1641
1807
  };
1642
1808
  let inheritedMcpToolsPublisher: ((tools: readonly CustomTool[]) => void) | undefined;
1643
1809
  let inheritedMcpToolsUnsubscribe: (() => void) | undefined;
@@ -1649,36 +1815,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1649
1815
  };
1650
1816
 
1651
1817
  try {
1652
- // Subscribe before any getApiKey() call so startup model probes can't fire a
1653
- // credential_disabled event past us. An embedder's constructor handler makes the
1654
- // listener set non-empty from construction, which defeats AuthStorage's no-listener
1655
- // buffer — so we can't rely on it to catch startup events for the extension runner.
1656
- const startupCredentialDisabledEvents: CredentialDisabledEvent[] = [];
1657
- let credentialDisabledTarget: ExtensionRunner | undefined;
1658
- unsubscribeCredentialDisabled = authStorage.onCredentialDisabled(event => {
1659
- if (credentialDisabledTarget) {
1660
- // Discard return: any handler error is routed through runner.onError listeners.
1661
- void credentialDisabledTarget.emitCredentialDisabled(event);
1662
- } else {
1663
- startupCredentialDisabledEvents.push(event);
1664
- }
1665
- });
1666
1818
  const applyCredentialSelector = (scopeId: string, provider: string, selector: AuthCredentialSelector): void => {
1667
1819
  authStorage.setSessionCredentialSelector(scopeId, provider, selector, authStorageOwner);
1668
1820
  };
1669
- const ownsScopedSettings = options.settings === undefined;
1670
- const settings = options.settings ?? (await logger.time("settings", Settings.loadForScope, { cwd, agentDir }));
1671
- if (ownsModelRegistry) modelRegistry.setScopedSettings(settings);
1672
1821
  const autoroutingInactive =
1673
1822
  settings.get("task.autorouting.enabled") === true && !settings.getEffectiveAutorouting().active;
1674
- closeOwnedSettings = async (): Promise<void> => {
1675
- if (!ownsScopedSettings) return;
1676
- try {
1677
- await settings.close();
1678
- } finally {
1679
- releaseSettingsScope(settings);
1680
- }
1681
- };
1682
1823
  // Cwd-derived runtime state must follow a rescope (`move_session`, `/move`),
1683
1824
  // so services resolve the LIVE session cwd per activation instead of
1684
1825
  // capturing the launch root. Before the manager exists the launch cwd is the
@@ -5825,67 +5966,102 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
5825
5966
  gjcRuntimeSnapshot: gjcRuntimeStore,
5826
5967
  };
5827
5968
  } catch (error) {
5828
- // Release the subscription if the throw happened after install but before the
5829
- // dispose-wrap took ownership.
5830
- stopInheritedMcpToolsSubscription();
5831
- releaseCredentialDisabledSubscription();
5832
5969
  let cleanupDiagnostic: unknown;
5833
- try {
5834
- if (hasSession) {
5970
+ let ownedMcpCleanupFailed = false;
5971
+ let ownedMcpCleanupError: unknown;
5972
+ const recordCleanupFailure = (cleanupError: unknown): void => {
5973
+ cleanupDiagnostic =
5974
+ cleanupDiagnostic === undefined
5975
+ ? cleanupError
5976
+ : new AggregateError([cleanupDiagnostic, cleanupError], "Multiple startup cleanup operations failed.");
5977
+ };
5978
+ const attemptCleanup = async (
5979
+ cleanup: () => void | Promise<void>,
5980
+ onFailure?: (cleanupError: unknown) => void,
5981
+ ): Promise<void> => {
5982
+ try {
5983
+ await cleanup();
5984
+ } catch (cleanupError) {
5985
+ recordCleanupFailure(cleanupError);
5986
+ onFailure?.(cleanupError);
5987
+ }
5988
+ };
5989
+ // Release the subscription if the throw happened after install but before the
5990
+ // dispose-wrap took ownership. Each independent cleanup still runs if another
5991
+ // teardown reports an error, and every failure is retained for lifecycle proof.
5992
+ if (!hasSession) preserveCredentialDisabledEventsForRetry();
5993
+ await attemptCleanup(stopInheritedMcpToolsSubscription);
5994
+ await attemptCleanup(releaseCredentialDisabledSubscription);
5995
+ if (hasSession) {
5996
+ await attemptCleanup(async () => {
5835
5997
  try {
5836
5998
  await session.dispose();
5837
5999
  } catch (disposeError) {
5838
- if (!isSessionDisposalIncompleteError(disposeError)) {
5839
- throw disposeError;
5840
- }
6000
+ if (!isSessionDisposalIncompleteError(disposeError)) throw disposeError;
5841
6001
  // The first call is intentionally bounded for startup callers. Join the
5842
6002
  // retained teardown before releasing the resources it still owns.
5843
6003
  await session.awaitDisposeCompletion();
5844
6004
  }
5845
- } else {
5846
- if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
5847
- // Admission happens before session construction. Any later startup
5848
- // failure must eventually release THIS manager's endpoint mapping
5849
- // through disposal and restore the prior global only when this manager
5850
- // is still global: otherwise a retry under the same endpoint is falsely
5851
- // rejected and an orphan redirects global-manager consumers away from
5852
- // the live session (review thread P1).
5853
- if (asyncJobManagerOwned && asyncJobManager) {
5854
- if (asyncJobManagerAdmitted) {
5855
- if (AsyncJobManager.instance() === asyncJobManager) {
6005
+ });
6006
+ } else {
6007
+ if (hasRegistered)
6008
+ await attemptCleanup(() => {
6009
+ agentRegistry.unregister(resolvedAgentId);
6010
+ });
6011
+ // Admission happens before session construction. Any later startup failure
6012
+ // must release this manager's endpoint mapping; a failed restoration cannot
6013
+ // prevent disposal of the owned manager or other startup resources.
6014
+ if (asyncJobManagerOwned && asyncJobManager) {
6015
+ const ownedAsyncJobManager = asyncJobManager;
6016
+ if (asyncJobManagerAdmitted)
6017
+ await attemptCleanup(() => {
6018
+ if (AsyncJobManager.instance() === ownedAsyncJobManager)
5856
6019
  AsyncJobManager.setInstance(priorAsyncJobManager);
5857
- }
5858
- }
5859
- await asyncJobManager.dispose({ timeoutMs: 100 });
5860
- }
5861
- await cleanupOwnedMcpManager?.();
5862
- const [{ disposeKernelSessionsByOwner }, { disposeVmContextsByOwner }] = await Promise.all([
5863
- import("../eval/py/executor"),
5864
- import("../eval/js/context-manager"),
5865
- ]);
5866
- await disposeKernelSessionsByOwner(evalKernelOwnerId);
5867
- await disposeVmContextsByOwner(evalKernelOwnerId);
5868
- await closeOwnedSettings();
6020
+ });
6021
+ await attemptCleanup(async () => {
6022
+ await ownedAsyncJobManager.dispose({ timeoutMs: 100 });
6023
+ });
5869
6024
  }
5870
- } catch (cleanupError) {
5871
- cleanupDiagnostic = cleanupError;
6025
+ if (cleanupOwnedMcpManager)
6026
+ await attemptCleanup(cleanupOwnedMcpManager, cleanupError => {
6027
+ ownedMcpCleanupFailed = true;
6028
+ ownedMcpCleanupError = cleanupError;
6029
+ });
6030
+ const evalCleanup = Promise.all([import("../eval/py/executor"), import("../eval/js/context-manager")]);
6031
+ let evalCleanupModules: Awaited<typeof evalCleanup> | undefined;
6032
+ try {
6033
+ evalCleanupModules = await evalCleanup;
6034
+ } catch (cleanupError) {
6035
+ recordCleanupFailure(cleanupError);
6036
+ }
6037
+ if (evalCleanupModules) {
6038
+ const [kernelExecutor, contextManager] = evalCleanupModules;
6039
+ await attemptCleanup(() => kernelExecutor.disposeKernelSessionsByOwner(evalKernelOwnerId));
6040
+ await attemptCleanup(() => contextManager.disposeVmContextsByOwner(evalKernelOwnerId));
6041
+ }
6042
+ await attemptCleanup(closeOwnedSettings);
6043
+ }
6044
+ if (processCwdClaimed)
6045
+ await attemptCleanup(() => {
6046
+ SessionManager.releaseProcessCwdOwnership(sessionManager);
6047
+ processCwdClaimed = false;
6048
+ });
6049
+ await attemptCleanup(releaseLocalProtocolOverride);
6050
+ await attemptCleanup(closeOwnedAuthStorage);
6051
+ if (cleanupDiagnostic !== undefined) {
5872
6052
  logger.warn("Failed to clean up createAgentSession resources after startup error", {
5873
6053
  error: safeErrorForLog(error),
5874
- cleanupDiagnostic: safeCleanupDiagnosticForLog(cleanupDiagnostic),
6054
+ cleanupDiagnostic: ownedMcpCleanupFailed
6055
+ ? safeCleanupDiagnosticForLog({ code: "MCP_MANAGER_CLEANUP_FAILED", cause: ownedMcpCleanupError })
6056
+ : safeErrorForLog(cleanupDiagnostic),
5875
6057
  });
5876
- } finally {
5877
- if (processCwdClaimed) {
5878
- SessionManager.releaseProcessCwdOwnership(sessionManager);
5879
- processCwdClaimed = false;
5880
- }
5881
- releaseLocalProtocolOverride();
5882
- try {
5883
- await closeOwnedAuthStorage();
5884
- } catch (authCleanupError) {
5885
- logger.warn("Failed to close owned auth storage after startup error", { error: authCleanupError });
6058
+ if (ownedMcpCleanupFailed) {
6059
+ const withMcpCleanupDiagnostic = attachMcpCleanupDiagnostic(error, ownedMcpCleanupError);
6060
+ if (cleanupDiagnostic === ownedMcpCleanupError) throw withMcpCleanupDiagnostic;
6061
+ throw attachStartupCleanupDiagnostic(withMcpCleanupDiagnostic, cleanupDiagnostic);
5886
6062
  }
6063
+ throw attachStartupCleanupDiagnostic(error, cleanupDiagnostic);
5887
6064
  }
5888
- if (cleanupDiagnostic !== undefined) throw attachMcpCleanupDiagnostic(error, cleanupDiagnostic);
5889
6065
  throw error;
5890
6066
  }
5891
6067
  }
@@ -177,8 +177,9 @@ export class SdkStartupCapability {
177
177
  return this.#cancelled;
178
178
  }
179
179
 
180
- cancel(): void {
180
+ cancel(failure?: SdkStartupFailure): void {
181
181
  this.#cancelled = true;
182
+ if (failure) this.settleFailure(failure);
182
183
  }
183
184
 
184
185
  /**