@gajae-code/coding-agent 0.16.3 → 0.16.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 (133) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  35. package/dist/types/sdk/session-api-key.d.ts +26 -0
  36. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +9 -0
  38. package/dist/types/setup/paseo/announce.d.ts +111 -0
  39. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  40. package/dist/types/system-prompt.d.ts +2 -0
  41. package/dist/types/task/executor.d.ts +2 -1
  42. package/dist/types/tools/fetch.d.ts +7 -0
  43. package/dist/types/tools/index.d.ts +8 -0
  44. package/dist/types/tools/path-utils.d.ts +2 -0
  45. package/dist/types/tools/search.d.ts +6 -0
  46. package/package.json +7 -7
  47. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  48. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  49. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  50. package/src/cli/args.ts +20 -9
  51. package/src/cli/root-flags.ts +5 -3
  52. package/src/config/autorouting-tier-map.ts +1 -0
  53. package/src/config/file-lock.ts +181 -17
  54. package/src/config/model-preset-registry.ts +5 -3
  55. package/src/config/model-profiles.ts +40 -0
  56. package/src/config/model-registry.ts +57 -11
  57. package/src/config/model-resolver.ts +48 -0
  58. package/src/config/settings-schema.ts +33 -1
  59. package/src/config/settings.ts +1 -0
  60. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  61. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  62. package/src/discovery/builtin.ts +13 -4
  63. package/src/discovery/claude-plugins.ts +12 -4
  64. package/src/discovery/helpers.ts +10 -2
  65. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  66. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  67. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  68. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  69. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  70. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  71. package/src/extensibility/shared-events.ts +1 -1
  72. package/src/extensibility/skills.ts +44 -16
  73. package/src/gjc-runtime/repository-binding.ts +1 -1
  74. package/src/gjc-runtime/session-state-lock.ts +92 -2
  75. package/src/internal-urls/docs-index.generated.ts +7 -7
  76. package/src/main.ts +72 -34
  77. package/src/modes/DESIGN.md +54 -0
  78. package/src/modes/components/assistant-message.ts +22 -9
  79. package/src/modes/components/model-selector.ts +166 -33
  80. package/src/modes/components/settings-selector.ts +1075 -235
  81. package/src/modes/components/status-line/command.ts +190 -0
  82. package/src/modes/components/status-line/index.ts +1 -0
  83. package/src/modes/components/status-line/segments.ts +32 -5
  84. package/src/modes/components/status-line/types.ts +5 -0
  85. package/src/modes/components/tool-status-header.ts +393 -21
  86. package/src/modes/controllers/command-controller.ts +2 -0
  87. package/src/modes/controllers/event-controller.ts +148 -5
  88. package/src/modes/controllers/selector-controller.ts +21 -0
  89. package/src/modes/interactive-mode.ts +47 -1
  90. package/src/modes/types.ts +1 -0
  91. package/src/modes/utils/ui-helpers.ts +1 -0
  92. package/src/prompts/system/system-prompt.md +8 -0
  93. package/src/prompts/tools/search.md +2 -0
  94. package/src/runtime-mcp/client.ts +60 -17
  95. package/src/runtime-mcp/manager.ts +171 -14
  96. package/src/runtime-mcp/pool.test.ts +209 -0
  97. package/src/runtime-mcp/pool.ts +224 -23
  98. package/src/runtime-mcp/transports/stdio.ts +158 -24
  99. package/src/runtime-mcp/types.ts +24 -0
  100. package/src/sdk/broker/broker.ts +5 -4
  101. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  102. package/src/sdk/bus/chat-daemon-runtime.ts +37 -1
  103. package/src/sdk/bus/slack-daemon.ts +92 -2
  104. package/src/sdk/bus/slack-live-provider.ts +12 -0
  105. package/src/sdk/bus/slack-provider.ts +5 -0
  106. package/src/sdk/cli/session-cli.ts +9 -6
  107. package/src/sdk/host/session-runtime.test.ts +2 -2
  108. package/src/sdk/host/session-runtime.ts +275 -43
  109. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  110. package/src/sdk/session-api-key.ts +94 -0
  111. package/src/sdk/session.ts +73 -29
  112. package/src/sdk/transport/serve-cli.ts +3 -3
  113. package/src/session/agent-session.ts +123 -38
  114. package/src/session/contribution-prep.ts +214 -21
  115. package/src/setup/paseo/announce.ts +450 -0
  116. package/src/setup/paseo/setup-deps.ts +15 -3
  117. package/src/slash-commands/builtin-registry.ts +1 -0
  118. package/src/system-prompt.ts +4 -0
  119. package/src/task/executor.ts +2 -1
  120. package/src/task/index.ts +17 -4
  121. package/src/tools/bash.ts +35 -8
  122. package/src/tools/cron.ts +1 -1
  123. package/src/tools/fetch.ts +29 -1
  124. package/src/tools/index.ts +8 -0
  125. package/src/tools/job.ts +8 -2
  126. package/src/tools/monitor.ts +8 -2
  127. package/src/tools/path-utils.ts +7 -0
  128. package/src/tools/search.ts +116 -58
  129. package/src/tools/skill.ts +22 -2
  130. package/src/tools/subagent.ts +3 -1
  131. package/src/tools/tool-catalog.generated.ts +8 -1
  132. package/src/web/insane/bridge.ts +113 -23
  133. package/src/web/search/providers/perplexity.ts +102 -41
@@ -0,0 +1,94 @@
1
+ /**
2
+ * AgentLoop calls `getApiKey(provider)` before `streamFn`.
3
+ * Resolve/stream already use `getApiKey(model)`. If those two lookups diverge,
4
+ * the child dies locally (~8ms, 0 tokens, sanitized "Agent run failed.")
5
+ * and never reaches the working model-scoped path (#5081).
6
+ */
7
+ export type SessionApiKeyModel = {
8
+ provider: string;
9
+ baseUrl?: string;
10
+ };
11
+
12
+ export type SessionApiKeyRegistry = {
13
+ getApiKey(
14
+ model: SessionApiKeyModel,
15
+ sessionId?: string,
16
+ options?: { signal?: AbortSignal },
17
+ ): Promise<string | undefined>;
18
+ getApiKeyForProvider(
19
+ provider: string,
20
+ sessionId?: string,
21
+ baseUrl?: string,
22
+ options?: { signal?: AbortSignal },
23
+ ): Promise<string | undefined>;
24
+ };
25
+
26
+ export type SessionApiKeyResolution = {
27
+ apiKey: string;
28
+ credentialSessionId: string | undefined;
29
+ };
30
+
31
+ export type SessionApiKeyCredentialType = "api_key" | "oauth" | undefined;
32
+
33
+ export function isStableSessionApiKeyCredentialType(
34
+ previous: SessionApiKeyCredentialType,
35
+ replacement: SessionApiKeyCredentialType,
36
+ ): boolean {
37
+ return previous === replacement;
38
+ }
39
+
40
+ export function resolveLiveSessionApiKeyModel(
41
+ live: SessionApiKeyModel | undefined,
42
+ captured: SessionApiKeyModel | undefined,
43
+ provider: string,
44
+ ): SessionApiKeyModel | undefined {
45
+ if (live?.provider === provider) return live;
46
+ if (captured?.provider === provider) return captured;
47
+ return undefined;
48
+ }
49
+
50
+ async function lookupOnce(
51
+ registry: SessionApiKeyRegistry,
52
+ provider: string,
53
+ sessionId: string | undefined,
54
+ model: SessionApiKeyModel | undefined,
55
+ signal: AbortSignal | undefined,
56
+ ): Promise<string | undefined> {
57
+ signal?.throwIfAborted();
58
+ const matchingModel = model?.provider === provider ? model : undefined;
59
+ const options = signal ? { signal } : undefined;
60
+ let key: string | undefined;
61
+ if (matchingModel) {
62
+ key = await registry.getApiKey(matchingModel, sessionId, options);
63
+ signal?.throwIfAborted();
64
+ }
65
+ if (!key) {
66
+ key = await registry.getApiKeyForProvider(provider, sessionId, matchingModel?.baseUrl, options);
67
+ signal?.throwIfAborted();
68
+ }
69
+ return key;
70
+ }
71
+
72
+ export async function lookupSessionApiKey(
73
+ registry: SessionApiKeyRegistry,
74
+ provider: string,
75
+ sessionId: string | undefined,
76
+ model: SessionApiKeyModel | undefined,
77
+ allowUnscopedRetry = true,
78
+ signal?: AbortSignal,
79
+ ): Promise<SessionApiKeyResolution> {
80
+ let key = await lookupOnce(registry, provider, sessionId, model, signal);
81
+ // Architect children inherit the parent SID. Direct `-p --no-session` does
82
+ // not. Scoped miss then global/broker hit is the remaining 0-token death
83
+ // after #5105 (jsonl: Agent run failed / output 0).
84
+ if (!key && sessionId && allowUnscopedRetry) {
85
+ key = await lookupOnce(registry, provider, undefined, model, signal);
86
+ if (key) return { apiKey: key, credentialSessionId: undefined };
87
+ }
88
+ if (!key) {
89
+ throw Object.assign(new Error(`No API key found for provider "${provider}"`), {
90
+ code: "provider_unavailable",
91
+ });
92
+ }
93
+ return { apiKey: key, credentialSessionId: sessionId };
94
+ }
@@ -197,6 +197,11 @@ import { EventBus } from "../utils/event-bus";
197
197
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
198
198
  import type { WorkspaceTree } from "../workspace-tree";
199
199
  import { createSessionLifecycleService } from "./lifecycle/client";
200
+ import {
201
+ isStableSessionApiKeyCredentialType,
202
+ lookupSessionApiKey,
203
+ resolveLiveSessionApiKeyModel,
204
+ } from "./session-api-key";
200
205
  import {
201
206
  attachLifecycleStartupCapability,
202
207
  lifecycleMcpStartupTimeoutOption,
@@ -2338,15 +2343,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2338
2343
  }
2339
2344
  if (options.skills === undefined && settings.get("skills.enabled")) {
2340
2345
  try {
2341
- const reloaded = await loadSkills({
2342
- ...settings.getGroup("skills"),
2343
- agentDir,
2344
- cwd: to,
2345
- disabledExtensions: settings.get("disabledExtensions"),
2346
- });
2347
- skills = withEmbeddedDefaultGjcSkills(reloaded.skills);
2348
- if (!options.parentTaskPrefix) setActiveSkills(skills);
2349
- await session?.replaceSkills(skills);
2346
+ await session?.reloadSkills(to);
2350
2347
  } catch (error) {
2351
2348
  logger.warn("Failed to reload skills after session rescope", { error: safeErrorForLog(error) });
2352
2349
  }
@@ -2708,13 +2705,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2708
2705
  else signal.addEventListener("abort", () => resolve(), { once: true });
2709
2706
  return promise;
2710
2707
  },
2708
+ getSessionId: () => sessionManager.getSessionId(),
2711
2709
  // Subagents inherit the parent's manager; its registered endpoint is
2712
2710
  // authoritative for owned-registration keying and endpoint-first
2713
2711
  // manager lookup (the child's own id is never registered), so tools
2714
2712
  // pass the same endpoint the manager's completion callback resolves
2715
2713
  // (review thread P1). For a top-level session this equals the
2716
2714
  // session id.
2717
- getSessionId: () => AsyncJobManager.endpointIdOf(asyncJobManager) ?? asyncJobEndpointId,
2715
+ getAsyncEndpointId: () => AsyncJobManager.endpointIdOf(asyncJobManager) ?? asyncJobEndpointId,
2718
2716
  registerForegroundFoldParticipant: adapter =>
2719
2717
  session?.registerForegroundFoldParticipant(adapter) ?? (() => {}),
2720
2718
  hasForegroundBashBackgroundRequestHandler: () => session?.hasForegroundBashBackgroundRequestHandler() ?? false,
@@ -3924,6 +3922,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3924
3922
  appendSystemPrompt: appendPrompt,
3925
3923
  pluginAppendices: pluginSystemAppendices,
3926
3924
  repeatToolDescriptions,
3925
+ reasoningLanguage: settings.get("reasoningLanguage"),
3927
3926
  intentField,
3928
3927
  toolDiscoveryActive: effectiveDiscoveryMode === "all" || mcpDiscoveryEnabled,
3929
3928
  eagerTasks: resolveEagerTasks(),
@@ -4257,6 +4256,32 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4257
4256
  ).seedNormalizedMessages(options.forkContextSeed.messages);
4258
4257
  }
4259
4258
 
4259
+ const resolvedCredentialScopes = new Map<string, string | undefined>();
4260
+ const resolveAgentApiKey = async (provider: string, signal?: AbortSignal): Promise<string> => {
4261
+ const liveModel = resolveLiveSessionApiKeyModel(sessionAgent?.state.model, model, provider);
4262
+ const hasExplicitScopePolicy =
4263
+ modelRegistry.authStorage.hasSessionCredentialSelector(provider, credentialSessionId) ||
4264
+ modelRegistry.authStorage.hasSessionCredentialAuto(provider, credentialSessionId);
4265
+ const resolution = await lookupSessionApiKey(
4266
+ modelRegistry,
4267
+ provider,
4268
+ credentialSessionId,
4269
+ liveModel,
4270
+ !hasExplicitScopePolicy,
4271
+ signal,
4272
+ );
4273
+ resolvedCredentialScopes.set(provider, resolution.credentialSessionId);
4274
+ return resolution.apiKey;
4275
+ };
4276
+ const resolvedCredentialType = (provider: string): "api_key" | "oauth" | undefined => {
4277
+ const scope = resolvedCredentialScopes.has(provider)
4278
+ ? resolvedCredentialScopes.get(provider)
4279
+ : credentialSessionId;
4280
+ return modelRegistry.authStorage.getEffectiveCredentialType(provider, scope, {
4281
+ owner: modelRegistry.getAuthStorageOwner(),
4282
+ });
4283
+ };
4284
+
4260
4285
  agent = new Agent({
4261
4286
  initialState: {
4262
4287
  systemPrompt,
@@ -4295,44 +4320,42 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4295
4320
  shouldPause: options.shouldPause,
4296
4321
  preferWebsockets: preferOpenAICodexWebsockets,
4297
4322
  getToolContext: tc => toolContextStore.getContext(tc),
4298
- getApiKey: async provider => {
4323
+ getApiKey: provider => {
4299
4324
  // AgentLoop asks by provider, but the active model carries the
4300
4325
  // model-scoped credential selector. Read it at call time so model
4301
4326
  // changes are honored after /new, fork, resume, or branch switches.
4302
- const liveModel = sessionAgent?.state.model ?? model;
4303
- const key =
4304
- liveModel?.provider === provider
4305
- ? await modelRegistry.getApiKey(liveModel, credentialSessionId)
4306
- : undefined;
4307
- const providerKey = key ?? (await modelRegistry.getApiKeyForProvider(provider, credentialSessionId));
4308
- if (!providerKey) {
4309
- throw new Error(`No API key found for provider "${provider}"`);
4310
- }
4311
- return providerKey;
4327
+ return resolveAgentApiKey(provider);
4312
4328
  },
4313
- getAuthCredentialType: provider => modelRegistry.getSessionCredentialType(provider, credentialSessionId),
4329
+ getAuthCredentialType: resolvedCredentialType,
4314
4330
  streamFn: async (streamModel, context, streamOptions) => {
4315
4331
  const requestStartedAt = performance.now();
4316
4332
  let stream: Awaited<ReturnType<typeof streamSimple>>;
4317
4333
  try {
4318
- const effectiveApiKey = await modelRegistry.getApiKey(streamModel, credentialSessionId);
4319
4334
  stream = await streamSimple(streamModel, context, {
4320
4335
  ...streamOptions,
4321
- apiKey: effectiveApiKey,
4322
- authCredentialType: modelRegistry.getSessionCredentialType(streamModel.provider, credentialSessionId),
4323
4336
  onAuthError: async (provider, oldKey, error) => {
4337
+ const resolvedScope = resolvedCredentialScopes.has(provider)
4338
+ ? resolvedCredentialScopes.get(provider)
4339
+ : credentialSessionId;
4324
4340
  await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, {
4325
4341
  signal: streamOptions?.signal,
4326
- sessionId: credentialSessionId,
4342
+ sessionId: resolvedScope,
4327
4343
  owner: modelRegistry.getAuthStorageOwner(),
4328
4344
  });
4329
4345
  logger.debug("Retrying provider request after credential invalidation", {
4330
4346
  provider,
4331
4347
  error: error instanceof Error ? error.message : String(error),
4332
4348
  });
4333
- return modelRegistry.getApiKey(streamModel, credentialSessionId, {
4334
- signal: streamOptions?.signal,
4335
- });
4349
+ const replacementApiKey = await resolveAgentApiKey(provider, streamOptions?.signal);
4350
+ if (
4351
+ !isStableSessionApiKeyCredentialType(
4352
+ streamOptions?.authCredentialType,
4353
+ resolvedCredentialType(provider),
4354
+ )
4355
+ ) {
4356
+ return undefined;
4357
+ }
4358
+ return replacementApiKey;
4336
4359
  },
4337
4360
  });
4338
4361
  } catch (error) {
@@ -4459,6 +4482,27 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4459
4482
  customCommands: customCommandsResult.commands,
4460
4483
  skills,
4461
4484
  skillWarnings,
4485
+ reloadSkills:
4486
+ options.skills === undefined
4487
+ ? async reloadCwd => {
4488
+ if (!settings.get("skills.enabled")) {
4489
+ return { skills: getEmbeddedDefaultGjcSkills(), warnings: [] };
4490
+ } else {
4491
+ const reloaded = await loadSkills({
4492
+ ...settings.getGroup("skills"),
4493
+ agentDir,
4494
+ cwd: reloadCwd,
4495
+ disabledExtensions: settings.get("disabledExtensions"),
4496
+ });
4497
+ return { skills: withEmbeddedDefaultGjcSkills(reloaded.skills), warnings: reloaded.warnings };
4498
+ }
4499
+ }
4500
+ : undefined,
4501
+ onSkillsReloaded: (reloadedSkills, reloadedWarnings) => {
4502
+ skills = reloadedSkills;
4503
+ skillWarnings = reloadedWarnings;
4504
+ if (!options.parentTaskPrefix) setActiveSkills(reloadedSkills);
4505
+ },
4462
4506
  skillsSettings: settings.getGroup("skills"),
4463
4507
  modelRegistry,
4464
4508
  taskDepth,
@@ -92,10 +92,10 @@ function brokerSessionRows(sessions: readonly unknown[]): BrokerSessionRow[] {
92
92
  }
93
93
 
94
94
  /** Exhausts strict broker `session.list` pages into one full session snapshot. */
95
- export async function listBrokerSessions(broker: SdkClient): Promise<BrokerSessionRow[]> {
95
+ export async function listBrokerSessions(broker: SdkClient, explicitSessionId?: string): Promise<BrokerSessionRow[]> {
96
96
  try {
97
97
  const pages = await traverseSessionList(
98
- {},
98
+ { ...(explicitSessionId ? { resolveSessionId: explicitSessionId } : {}) },
99
99
  async input => await broker.global("session.list", input),
100
100
  response => {
101
101
  brokerResult(response);
@@ -147,7 +147,7 @@ export async function runSdkServe(argv: string[]): Promise<void> {
147
147
  throw new Error("broker_unavailable: SDK broker is not reachable");
148
148
  }
149
149
  try {
150
- const sessionId = selectBrokerSession(await listBrokerSessions(broker), parsed.sessionId);
150
+ const sessionId = selectBrokerSession(await listBrokerSessions(broker, parsed.sessionId), parsed.sessionId);
151
151
  const endpoint = brokerResult(await broker.global("session.get_endpoint", { sessionId }));
152
152
  const url = typeof endpoint.url === "string" && endpoint.url ? endpoint.url : undefined;
153
153
  const token = typeof endpoint.token === "string" ? endpoint.token : "";
@@ -36,6 +36,7 @@ import {
36
36
  canContinuePersistedHistory,
37
37
  dispatchedToolIdentity,
38
38
  getAgentTerminalOwnerContext,
39
+ isContinuingMidRunMaintenanceOutcome,
39
40
  isNonDispatchedToolEvent,
40
41
  type ManagedAttemptContinuationOwnership,
41
42
  type ManagedAttemptDecision,
@@ -882,6 +883,10 @@ export interface AgentSessionConfig {
882
883
  skills?: Skill[];
883
884
  /** Skill loading warnings (already captured by SDK) */
884
885
  skillWarnings?: SkillWarning[];
886
+ /** Re-enumerate filesystem and marketplace skills after plugin lifecycle changes. */
887
+ reloadSkills?: (cwd: string) => Promise<{ skills: Skill[]; warnings: SkillWarning[] }>;
888
+ /** Publish a successfully committed skill catalog to SDK/process snapshots. */
889
+ onSkillsReloaded?: (skills: Skill[], warnings: SkillWarning[]) => void;
885
890
  /** Custom commands (TypeScript slash commands) */
886
891
  customCommands?: LoadedCustomCommand[];
887
892
  skillsSettings?: SkillsSettings;
@@ -1044,7 +1049,7 @@ type AutoCompactionTerminalStatus =
1044
1049
  | { kind: "compacted"; continuationScheduled?: boolean }
1045
1050
  | { kind: "aborted"; source: "signal" | "hook" }
1046
1051
  | { kind: "skipped"; continuationScheduled?: boolean }
1047
- | { kind: "failed" };
1052
+ | { kind: "failed"; errorMessage: string };
1048
1053
 
1049
1054
  type ToolOutputPruneResult = {
1050
1055
  prunedCount: number;
@@ -3124,6 +3129,8 @@ export class AgentSession {
3124
3129
 
3125
3130
  #skills: Skill[];
3126
3131
  #skillWarnings: SkillWarning[];
3132
+ #reloadSkills: AgentSessionConfig["reloadSkills"];
3133
+ #onSkillsReloaded: AgentSessionConfig["onSkillsReloaded"];
3127
3134
 
3128
3135
  // Custom commands (TypeScript slash commands)
3129
3136
  #customCommands: LoadedCustomCommand[] = [];
@@ -4556,6 +4563,8 @@ export class AgentSession {
4556
4563
  this.agent.setMainAttemptScopeObserver(scope => this.#bindAttemptScope(scope));
4557
4564
  this.#skills = config.skills ?? [];
4558
4565
  this.#skillWarnings = config.skillWarnings ?? [];
4566
+ this.#reloadSkills = config.reloadSkills;
4567
+ this.#onSkillsReloaded = config.onSkillsReloaded;
4559
4568
  this.#customCommands = config.customCommands ?? [];
4560
4569
  this.#skillsSettings = config.skillsSettings;
4561
4570
  this.#modelRegistry = config.modelRegistry;
@@ -5638,6 +5647,27 @@ export class AgentSession {
5638
5647
  if (failures.length > 0) throw new AggregateError(failures, "Tool session cleanup failed.");
5639
5648
  }
5640
5649
 
5650
+ #claimToolSessionTransitionCleanups(): Promise<void> {
5651
+ const cleanups = Array.from(this.#toolSessionTransitionCleanups);
5652
+ this.#toolSessionTransitionCleanups.clear();
5653
+ return this.#settleClaimedToolSessionCleanups(cleanups, "Tool session transition cleanup failed.");
5654
+ }
5655
+
5656
+ #claimToolSessionCleanups(): Promise<void> {
5657
+ const cleanups = Array.from(this.#toolSessionCleanups);
5658
+ this.#toolSessionCleanups.clear();
5659
+ return this.#settleClaimedToolSessionCleanups(cleanups, "Tool session cleanup failed.");
5660
+ }
5661
+
5662
+ async #settleClaimedToolSessionCleanups(
5663
+ cleanups: readonly (() => Promise<void> | void)[],
5664
+ message: string,
5665
+ ): Promise<void> {
5666
+ const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
5667
+ const failures = results.flatMap(result => (result.status === "rejected" ? [result.reason] : []));
5668
+ if (failures.length > 0) throw new AggregateError(failures, message);
5669
+ }
5670
+
5641
5671
  getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
5642
5672
  // The session's OWNED manager first: /jobs, the status header, and the
5643
5673
  // active-background count must reflect THIS session's jobs — with
@@ -5918,7 +5948,9 @@ export class AgentSession {
5918
5948
  const canonicalAdmission = this.#reserveCanonicalMessageAdmission(event);
5919
5949
  const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
5920
5950
  const maintenanceCheckpoint =
5921
- event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
5951
+ event.type === "agent_end" &&
5952
+ event.stopReason === "maintenance" &&
5953
+ isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome);
5922
5954
  const terminalClaim =
5923
5955
  maintenanceCheckpoint || !terminalOwner
5924
5956
  ? undefined
@@ -6196,9 +6228,13 @@ export class AgentSession {
6196
6228
  this.#requestWorkerIntegrationAttempt();
6197
6229
  }
6198
6230
  // A maintenance agent_end is an internal checkpoint only while another
6199
- // continuation will follow. An aborted maintenance run is its terminal
6200
- // settlement and must reach public subscribers.
6201
- if (event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
6231
+ // continuation will follow. Failed or aborted maintenance is terminal and
6232
+ // must reach public subscribers.
6233
+ if (
6234
+ event.type === "agent_end" &&
6235
+ event.stopReason === "maintenance" &&
6236
+ isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome)
6237
+ )
6202
6238
  return;
6203
6239
 
6204
6240
  // Hold agent_end until the prompt's finally and all earlier async event work
@@ -6983,7 +7019,7 @@ export class AgentSession {
6983
7019
 
6984
7020
  if (
6985
7021
  event.type === "agent_end" &&
6986
- !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
7022
+ !(event.stopReason === "maintenance" && isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome))
6987
7023
  ) {
6988
7024
  this.#releaseDeferredSdkFollowUps();
6989
7025
  }
@@ -6996,28 +7032,33 @@ export class AgentSession {
6996
7032
  // the same run on the rewritten context (no synthetic prompt), skipping
6997
7033
  // the goal-runtime / skill-state / retry / compaction finalization a
6998
7034
  // normal agent_end runs — none of that applies to an in-progress run.
6999
- // "aborted" settles without resuming; the generation guard drops the
7000
- // continuation if a newer prompt/abort has moved the run on.
7035
+ // Failed and aborted outcomes fall through to terminal handling. The
7036
+ // generation guard drops a successful continuation if a newer prompt/abort
7037
+ // has moved the run on.
7001
7038
  if (event.stopReason === "maintenance") {
7002
- this.#lastAssistantMessage = undefined;
7003
- const outcome = event.maintenanceOutcome;
7004
- if (
7005
- outcome &&
7006
- outcome !== "aborted" &&
7007
- !maintenanceWasDisposed &&
7008
- !this.#isDisposed &&
7009
- maintenanceGeneration !== undefined &&
7010
- this.#promptGeneration === maintenanceGeneration
7011
- ) {
7012
- this.#scheduleAgentContinue({
7013
- generation: maintenanceGeneration,
7014
- skipCompactionCheck: true,
7015
- resourceRunId: activePromptHandle,
7016
- maintenanceContinuation: true,
7017
- sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
7018
- });
7039
+ if (isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome)) {
7040
+ this.#lastAssistantMessage = undefined;
7041
+ if (
7042
+ !maintenanceWasDisposed &&
7043
+ !this.#isDisposed &&
7044
+ maintenanceGeneration !== undefined &&
7045
+ this.#promptGeneration === maintenanceGeneration
7046
+ ) {
7047
+ this.#scheduleAgentContinue({
7048
+ generation: maintenanceGeneration,
7049
+ skipCompactionCheck: true,
7050
+ resourceRunId: activePromptHandle,
7051
+ maintenanceContinuation: true,
7052
+ sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
7053
+ });
7054
+ }
7055
+ return;
7019
7056
  }
7020
- return;
7057
+ // Cancellation already owns teardown and terminal publication. Running
7058
+ // normal post-turn finalization here can deadlock abort/dispose while the
7059
+ // maintenance barrier is unwinding. Failed maintenance alone carries the
7060
+ // synthetic assistant error that must flow through terminal handling.
7061
+ if (event.maintenanceOutcome === "aborted") return;
7021
7062
  }
7022
7063
  const usage = this.getSessionStats().tokens;
7023
7064
  await this.#goalRuntime.onAgentEnd({
@@ -7083,6 +7124,13 @@ export class AgentSession {
7083
7124
  return;
7084
7125
  }
7085
7126
  this.#lastSuccessfulYieldToolCallId = undefined;
7127
+ if (event.stopReason === "maintenance" && event.maintenanceOutcome === "failed") {
7128
+ // The maintenance error already terminalized the run. Retrying this
7129
+ // synthetic assistant failure would resubmit the unchanged context that
7130
+ // maintenance failed to rewrite.
7131
+ this.#resolveRetry();
7132
+ return;
7133
+ }
7086
7134
 
7087
7135
  // Check for retryable errors first (overloaded, rate limit, server errors)
7088
7136
  if (this.#isRetryableError(msg)) {
@@ -8580,7 +8628,8 @@ export class AgentSession {
8580
8628
  ? this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope)
8581
8629
  : undefined;
8582
8630
  const isTerminalAgentEnd =
8583
- event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
8631
+ event.type === "agent_end" &&
8632
+ !(event.stopReason === "maintenance" && isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome));
8584
8633
  const finishAttempt = () => {
8585
8634
  if (!isTerminalAgentEnd) return;
8586
8635
  // A merged wake batch whose flush fired while still streaming cleared its
@@ -9371,10 +9420,10 @@ export class AgentSession {
9371
9420
  // one here left signal exit orphaning the subprocess. Each runner clears
9372
9421
  // its set, so a graceful dispose running first makes this a no-op rather
9373
9422
  // than a double free.
9374
- this.#runToolSessionCleanups().catch((error: unknown) =>
9423
+ this.#claimToolSessionCleanups().catch((error: unknown) =>
9375
9424
  logger.warn("signal teardown: tool session cleanups failed", { error }),
9376
9425
  ),
9377
- this.#runToolSessionTransitionCleanups().catch((error: unknown) =>
9426
+ this.#claimToolSessionTransitionCleanups().catch((error: unknown) =>
9378
9427
  logger.warn("signal teardown: tool session transition cleanups failed", { error }),
9379
9428
  ),
9380
9429
  ]);
@@ -14200,6 +14249,25 @@ export class AgentSession {
14200
14249
  await this.refreshBaseSystemPrompt();
14201
14250
  }
14202
14251
 
14252
+ /** Refresh the live registered-skill catalog after install/remove/reload. */
14253
+ async reloadSkills(cwd = this.sessionManager.getCwd()): Promise<void> {
14254
+ if (!this.#reloadSkills) return;
14255
+ const reloaded = await this.#reloadSkills(cwd);
14256
+ const previousSkills = this.#skills;
14257
+ const previousWarnings = this.#skillWarnings;
14258
+ this.#skills = reloaded.skills;
14259
+ this.#skillWarnings = reloaded.warnings;
14260
+ try {
14261
+ await this.refreshBaseSystemPrompt();
14262
+ } catch (error) {
14263
+ this.#skills = previousSkills;
14264
+ this.#skillWarnings = previousWarnings;
14265
+ await this.refreshBaseSystemPrompt().catch(() => {});
14266
+ throw error;
14267
+ }
14268
+ this.#onSkillsReloaded?.(reloaded.skills, reloaded.warnings);
14269
+ }
14270
+
14203
14271
  /**
14204
14272
  * Retire the cached workspace tree after a rescope so the next turn re-scans
14205
14273
  * at the new cwd instead of re-presenting the abandoned launcher root.
@@ -18407,18 +18475,23 @@ export class AgentSession {
18407
18475
  * prune → promote → compact machinery and returns an explicit outcome.
18408
18476
  *
18409
18477
  * A non-"not-needed" outcome ends the current run losslessly
18410
- * (`agent_end.stopReason === "maintenance"`); the `agent_end` handler is the
18411
- * single continuation owner that resumes the run with no synthetic prompt.
18412
- * This method never self-continues, runs goal-runtime hooks, clears skill
18413
- * state, or emits a user-facing pause.
18478
+ * (`agent_end.stopReason === "maintenance"`). The `agent_end` handler resumes
18479
+ * committed maintenance with no synthetic prompt and terminalizes failed or
18480
+ * aborted maintenance. This method never self-continues, runs goal-runtime
18481
+ * hooks, clears skill state, or emits a user-facing pause.
18414
18482
  */
18415
18483
  async #runMidRunMaintenance(
18416
18484
  context: AgentContext,
18417
18485
  lifecycle: MidRunMaintenanceLifecycle,
18418
18486
  ): Promise<ContextMaintenanceResult> {
18419
- const result = (outcome: MidRunMaintenanceOutcome, releaseCurrentContext = false): ContextMaintenanceResult => ({
18487
+ const result = (
18488
+ outcome: MidRunMaintenanceOutcome,
18489
+ releaseCurrentContext = false,
18490
+ errorMessage?: string,
18491
+ ): ContextMaintenanceResult => ({
18420
18492
  outcome,
18421
18493
  ...(releaseCurrentContext ? { releaseCurrentContext: true } : {}),
18494
+ ...(errorMessage ? { errorMessage } : {}),
18422
18495
  });
18423
18496
  if (this.#isDisposed) return { outcome: "aborted" };
18424
18497
  const invocationController = new AbortController();
@@ -18529,7 +18602,13 @@ export class AgentSession {
18529
18602
  if (compactionStatus.kind === "aborted") {
18530
18603
  return compactionStatus.source === "hook" ? result("not-needed") : result("aborted");
18531
18604
  }
18532
- return result("failed");
18605
+ return result(
18606
+ "failed",
18607
+ false,
18608
+ compactionStatus.kind === "failed"
18609
+ ? compactionStatus.errorMessage
18610
+ : "Context maintenance could not commit a compaction before the next model request.",
18611
+ );
18533
18612
  } finally {
18534
18613
  this.#activeMidRunBarrierControllers.delete(invocationController);
18535
18614
  }
@@ -19643,12 +19722,15 @@ export class AgentSession {
19643
19722
  */
19644
19723
  #maintenanceProviderTransport(): {
19645
19724
  sessionId: string | undefined;
19725
+ providerSessionId: string | undefined;
19646
19726
  providerSessionState: Map<string, ProviderSessionState>;
19647
19727
  preferWebsockets: boolean | undefined;
19648
19728
  remoteCompactionFallbackHealth: RemoteCompactionFallbackHealthHooks;
19649
19729
  } {
19730
+ const providerSessionId = this.agent.providerSessionId ?? this.agent.sessionId;
19650
19731
  return {
19651
- sessionId: this.agent.providerSessionId ?? this.agent.sessionId,
19732
+ sessionId: providerSessionId,
19733
+ providerSessionId,
19652
19734
  providerSessionState: this.#providerSessionState,
19653
19735
  preferWebsockets: this.agent.preferWebsockets,
19654
19736
  remoteCompactionFallbackHealth: this.#remoteCompactionFallbackHealth,
@@ -20286,7 +20368,7 @@ export class AgentSession {
20286
20368
  ? `Context overflow recovery failed: ${errorMessage}`
20287
20369
  : `Auto-compaction failed: ${errorMessage}`,
20288
20370
  });
20289
- return { kind: "failed" };
20371
+ return { kind: "failed", errorMessage };
20290
20372
  } finally {
20291
20373
  if (this.#autoCompactionAbortController === autoCompactionAbortController) {
20292
20374
  this.#autoCompactionAbortController = undefined;
@@ -22350,7 +22432,10 @@ export class AgentSession {
22350
22432
  onChunk,
22351
22433
  signal: abortController.signal,
22352
22434
  });
22353
- this.recordPythonResult(code, result, options);
22435
+ // A retained kernel can finish after strict session close has fenced
22436
+ // persistence. The caller still receives its completed result, but it
22437
+ // must not reopen a closing session to append history.
22438
+ if (!this.#evalExecutionDisposing) this.recordPythonResult(code, result, options);
22354
22439
  return result;
22355
22440
  })();
22356
22441
  return await this.trackEvalExecution(execution, abortController);