@gajae-code/coding-agent 0.11.10 → 0.11.11

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 (111) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/settings-schema.d.ts +57 -2
  6. package/dist/types/config/settings.d.ts +1 -0
  7. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  8. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  9. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  10. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  11. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  12. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  13. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  14. package/dist/types/modes/components/welcome.d.ts +1 -0
  15. package/dist/types/modes/interactive-mode.d.ts +5 -1
  16. package/dist/types/runtime/memory-domain.d.ts +9 -0
  17. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  18. package/dist/types/runtime/memory-guard.d.ts +39 -0
  19. package/dist/types/runtime/memory-limit.d.ts +11 -0
  20. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  21. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  22. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  23. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  24. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  25. package/dist/types/sdk/bus/index.d.ts +3 -1
  26. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  29. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  30. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  31. package/dist/types/sdk/startup-capability.d.ts +6 -0
  32. package/dist/types/session/agent-session.d.ts +9 -11
  33. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  34. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  35. package/dist/types/session/session-manager.d.ts +34 -0
  36. package/dist/types/task/executor.d.ts +6 -0
  37. package/dist/types/task/types.d.ts +36 -0
  38. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  39. package/dist/types/tools/resource-gc.d.ts +19 -0
  40. package/package.json +8 -8
  41. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  42. package/scripts/run-test-manifest.ts +6 -4
  43. package/src/cli/args.ts +14 -8
  44. package/src/cli/fast-help.ts +1 -1
  45. package/src/cli.ts +48 -2
  46. package/src/commands/sdk.ts +91 -7
  47. package/src/config/model-profile-activation.ts +1 -0
  48. package/src/config/model-profile-contract.ts +13 -0
  49. package/src/config/model-profiles.ts +24 -0
  50. package/src/config/settings-schema.ts +61 -2
  51. package/src/config/settings.ts +8 -0
  52. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  53. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  54. package/src/extensibility/extensions/runner.ts +4 -0
  55. package/src/extensibility/extensions/types.ts +14 -0
  56. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  57. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  58. package/src/gjc-runtime/linux-proc.ts +70 -36
  59. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  60. package/src/gjc-runtime/state-runtime.ts +48 -11
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +23 -4
  63. package/src/modes/acp/acp-agent.ts +554 -60
  64. package/src/modes/acp/acp-event-mapper.ts +93 -6
  65. package/src/modes/components/model-selector.ts +15 -1
  66. package/src/modes/components/notifications-settings-editor.ts +1 -1
  67. package/src/modes/components/queue-pane.ts +55 -9
  68. package/src/modes/components/queued-message-selector.ts +53 -10
  69. package/src/modes/components/tool-status-header.ts +1 -4
  70. package/src/modes/components/welcome.ts +18 -3
  71. package/src/modes/controllers/command-controller.ts +4 -0
  72. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  73. package/src/modes/controllers/input-controller.ts +44 -3
  74. package/src/modes/interactive-mode.ts +65 -26
  75. package/src/modes/runtime-init.ts +1 -0
  76. package/src/prompts/agents/executor.md +1 -1
  77. package/src/runtime/memory-domain.ts +66 -0
  78. package/src/runtime/memory-guard-contract.ts +73 -0
  79. package/src/runtime/memory-guard.ts +292 -0
  80. package/src/runtime/memory-limit.ts +50 -0
  81. package/src/runtime-mcp/manager.ts +26 -6
  82. package/src/sdk/acp/adapter.ts +8 -1
  83. package/src/sdk/acp/final-text.ts +40 -0
  84. package/src/sdk/acp/mcp.ts +26 -0
  85. package/src/sdk/broker/lifecycle.ts +82 -1
  86. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  87. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  88. package/src/sdk/bus/config.ts +1 -1
  89. package/src/sdk/bus/index.ts +162 -22
  90. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  91. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  92. package/src/sdk/bus/telegram-daemon.ts +531 -53
  93. package/src/sdk/host/control/dispatch.ts +2 -0
  94. package/src/sdk/host/control/operations.ts +1 -0
  95. package/src/sdk/host/host.ts +2 -2
  96. package/src/sdk/lifecycle-session.ts +19 -3
  97. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  98. package/src/sdk/protocol/operation-registry.ts +1 -0
  99. package/src/sdk/session.ts +12 -1
  100. package/src/sdk/startup-capability.ts +6 -0
  101. package/src/session/agent-session.ts +256 -142
  102. package/src/session/fallback-chain-controller.ts +47 -8
  103. package/src/session/internal/managed-session-scope.ts +40 -10
  104. package/src/session/internal/native-publish-outcome.ts +8 -3
  105. package/src/session/session-manager.ts +608 -25
  106. package/src/task/executor.ts +11 -3
  107. package/src/task/index.ts +2 -0
  108. package/src/task/types.ts +22 -11
  109. package/src/task/ultragoal-redteam-activation.ts +75 -0
  110. package/src/tools/resource-gc.ts +496 -120
  111. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -31,7 +31,8 @@ import { ThinkingLevel } from "@gajae-code/agent-core";
31
31
  import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
32
32
  import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
33
33
  import { logger, postmortem, VERSION } from "@gajae-code/utils";
34
- import { projectModelProfileCatalog } from "../../config/model-profile-contract";
34
+ import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
35
+ import { isAuthenticated, kNoAuth } from "../../config/model-registry";
35
36
  import { Settings } from "../../config/settings";
36
37
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
37
38
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
@@ -54,6 +55,7 @@ import type {
54
55
  AskSettlementResult,
55
56
  } from "../../tools";
56
57
  import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../../tools/ask-answer-registry";
58
+ import { acpFinalTextFromMessage } from "../acp/final-text";
57
59
  import { ensureBroker } from "../broker/ensure";
58
60
  import { SessionIndex } from "../broker/session-index";
59
61
  import { SessionSdkHost, shouldHostSdk } from "../host";
@@ -935,7 +937,13 @@ interface SessionRuntime {
935
937
  emitPromptLifecycle: (
936
938
  correlation: { commandId: string; turnId: string } | undefined,
937
939
  frame:
938
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
940
+ | {
941
+ type: "agent_start" | "agent_end";
942
+ sessionId: string;
943
+ commandId?: string;
944
+ turnId?: string;
945
+ finalText?: string;
946
+ }
939
947
  | {
940
948
  type: "agent_failed";
941
949
  sessionId: string;
@@ -975,7 +983,10 @@ interface SessionRuntime {
975
983
  /** SDK control frames received during provisional ownership; replayed only after stable activation. */
976
984
  deferredInboundControls: Array<() => void>;
977
985
  /** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
978
- inFlightTools: Map<string, { toolName: string; args: unknown }>;
986
+ inFlightTools: Map<
987
+ string,
988
+ { toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
989
+ >;
979
990
  /** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
980
991
  cancelPostmortemCleanup: () => void;
981
992
  /** Disposes side-turn resources when their owning logical session becomes unavailable. */
@@ -1105,7 +1116,7 @@ const defaultConfig: NotificationConfig = {
1105
1116
  idleTimeoutMs: 60_000,
1106
1117
  rich: { enabled: true },
1107
1118
  richDraft: { enabled: false },
1108
- toolActivity: { enabled: true },
1119
+ toolActivity: { enabled: false },
1109
1120
  streaming: { enabled: true },
1110
1121
  topics: {},
1111
1122
  btw: { enabled: true },
@@ -1698,6 +1709,7 @@ const UNINSTALLED_CONTROL_OPERATIONS = new Set(["auth.login", "host_tools.regist
1698
1709
 
1699
1710
  const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
1700
1711
  "model.cycle": "cycleModel",
1712
+ "model.profile.set": "setModelProfile",
1701
1713
  "thinking.cycle": "cycleThinkingLevel",
1702
1714
  "queue.steering_mode.set": "setQueueMode",
1703
1715
  "queue.follow_up_mode.set": "setQueueMode",
@@ -1862,8 +1874,26 @@ function sdkQuerySurface(
1862
1874
  const currentThinkingLevel = api.getThinkingLevel();
1863
1875
  return projectQ10Models({ models, currentModel, currentThinkingLevel });
1864
1876
  },
1865
- getModelProfiles: () =>
1866
- projectModelProfileCatalog(ctx.modelRegistry.getModelProfiles(), ctx.modelRegistry.getError()),
1877
+ getModelProfiles: async () => {
1878
+ const profiles = ctx.modelRegistry.getModelProfiles();
1879
+ const catalog = projectModelProfileCatalog(profiles, ctx.modelRegistry.getError());
1880
+ const providers = new Set([...profiles.values()].flatMap(profile => profile.requiredProviders));
1881
+ const authenticatedProviders = new Set<string>();
1882
+ await Promise.all(
1883
+ [...providers].map(async provider => {
1884
+ try {
1885
+ const credential = await ctx.modelRegistry.getApiKeyForProvider(provider, id);
1886
+ if (credential === kNoAuth || isAuthenticated(credential)) authenticatedProviders.add(provider);
1887
+ } catch {
1888
+ // A provider whose credential state cannot be read is not currently configurable.
1889
+ }
1890
+ }),
1891
+ );
1892
+ return catalog.map(item => ({
1893
+ ...item,
1894
+ available: isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders),
1895
+ }));
1896
+ },
1867
1897
  getSkillState: () => ctx.getSkillState(),
1868
1898
  getGates: () => {
1869
1899
  const workflowGate = ctx.workflowGate;
@@ -2333,6 +2363,14 @@ function sdkControlSurface(
2333
2363
  );
2334
2364
  return typed("model.set", { id: `${model.provider}/${model.id}`, thinkingLevel });
2335
2365
  },
2366
+ setModelProfile: async id => {
2367
+ if (!bindings.has("setModelProfile") || !ctx.setModelProfile)
2368
+ return unavailable("model.profile.set", "no model-profile activation seam is installed")();
2369
+ if (!id) throw Object.assign(new Error("model.profile.set requires a profile id."), { code: "invalid_input" });
2370
+ if (!ctx.modelRegistry.getModelProfile(id))
2371
+ throw Object.assign(new Error(`Model profile ${id} was not found.`), { code: "invalid_input" });
2372
+ return { changed: await ctx.setModelProfile(id), id };
2373
+ },
2336
2374
  cycleModel: async () => {
2337
2375
  if (!bindings.has("cycleModel"))
2338
2376
  return unavailable("model.cycle", "no session model-cycle seam is installed")();
@@ -3219,7 +3257,13 @@ export function createNotificationsExtension(
3219
3257
  const promptSubmissionKey = (correlation: { commandId: string; turnId: string }) =>
3220
3258
  `${correlation.commandId}:${correlation.turnId}`;
3221
3259
  type PromptLifecycleFrame =
3222
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
3260
+ | {
3261
+ type: "agent_start" | "agent_end";
3262
+ sessionId: string;
3263
+ commandId?: string;
3264
+ turnId?: string;
3265
+ finalText?: string;
3266
+ }
3223
3267
  | {
3224
3268
  type: "agent_failed";
3225
3269
  sessionId: string;
@@ -3669,7 +3713,10 @@ export function createNotificationsExtension(
3669
3713
  emitPromptLifecycle,
3670
3714
  emitPromptEvent,
3671
3715
  pendingInbound: new Set<number>(),
3672
- inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
3716
+ inFlightTools: new Map<
3717
+ string,
3718
+ { toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
3719
+ >(),
3673
3720
  deferredGatePresentations: [],
3674
3721
  deferredInboundControls: [],
3675
3722
  notificationRootRegistration: undefined,
@@ -4103,7 +4150,7 @@ export function createNotificationsExtension(
4103
4150
  }
4104
4151
  if (typeof inbound.redact === "boolean") {
4105
4152
  if (inbound.redact && !runtime.committedRedact) {
4106
- terminalizeInFlightTools(runtime, runtime.id, "unknown");
4153
+ terminalizeInFlightTools(runtime, runtime.id, "cancelled");
4107
4154
  }
4108
4155
  runtime.committedRedact = inbound.redact;
4109
4156
  runtime.redact = inbound.redact;
@@ -4450,8 +4497,15 @@ export function createNotificationsExtension(
4450
4497
  runtime.redact = true;
4451
4498
  runtime.verbosity = "lean";
4452
4499
  runtime.stream = false;
4500
+ for (const [toolCallId, tool] of runtime.inFlightTools) {
4501
+ runtime.inFlightTools.set(toolCallId, {
4502
+ toolName: tool.toolName,
4503
+ ...(tool.pendingPhase ? { pendingPhase: tool.pendingPhase } : {}),
4504
+ });
4505
+ }
4453
4506
  return;
4454
4507
  }
4508
+ const wasPolicySuspended = runtime.policySuspended;
4455
4509
  const redactionEnabled = policy.redact && !runtime.committedRedact;
4456
4510
  runtime.policyGeneration++;
4457
4511
  runtime.committedRedact = policy.redact;
@@ -4459,7 +4513,8 @@ export function createNotificationsExtension(
4459
4513
  runtime.redact = policy.redact;
4460
4514
  runtime.verbosity = policy.verbosity;
4461
4515
  runtime.stream = policy.stream;
4462
- if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "unknown");
4516
+ if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "cancelled", true);
4517
+ else if (wasPolicySuspended && !policy.redact) settleProvisionalToolTerminals(runtime, runtime.id);
4463
4518
  },
4464
4519
  activate: binding => {
4465
4520
  const runtime = runtimes.get(binding.sessionId);
@@ -4702,8 +4757,22 @@ export function createNotificationsExtension(
4702
4757
  await rotateSessionAuthority(event, ctx, false);
4703
4758
  });
4704
4759
 
4705
- const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
4706
- if (rt.notificationsActive && !rt.redact) {
4760
+ const terminalizeInFlightTools = (
4761
+ rt: SessionRuntime,
4762
+ id: string,
4763
+ phase: "cancelled" | "failed",
4764
+ allowSafeRedactedFrame = false,
4765
+ ): void => {
4766
+ if (rt.policySuspended && !allowSafeRedactedFrame) {
4767
+ for (const [toolCallId, tool] of rt.inFlightTools) {
4768
+ rt.inFlightTools.set(toolCallId, {
4769
+ toolName: tool.toolName,
4770
+ pendingPhase: tool.pendingPhase ?? phase,
4771
+ });
4772
+ }
4773
+ return;
4774
+ }
4775
+ if (rt.notificationsActive && (!rt.redact || allowSafeRedactedFrame)) {
4707
4776
  for (const [toolCallId, { toolName }] of rt.inFlightTools) {
4708
4777
  try {
4709
4778
  pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
@@ -4715,6 +4784,27 @@ export function createNotificationsExtension(
4715
4784
  rt.inFlightTools.clear();
4716
4785
  };
4717
4786
 
4787
+ const settleProvisionalToolTerminals = (rt: SessionRuntime, id: string): void => {
4788
+ for (const [toolCallId, tool] of rt.inFlightTools) {
4789
+ if (!tool.pendingPhase) continue;
4790
+ try {
4791
+ if (rt.notificationsActive && !rt.redact) {
4792
+ pushSessionFrame(rt, {
4793
+ type: "tool_activity",
4794
+ sessionId: id,
4795
+ toolCallId,
4796
+ toolName: tool.toolName,
4797
+ phase: tool.pendingPhase,
4798
+ });
4799
+ }
4800
+ } catch (e) {
4801
+ logger.warn(`notifications: provisional tool_activity settlement failed: ${String(e)}`);
4802
+ } finally {
4803
+ rt.inFlightTools.delete(toolCallId);
4804
+ }
4805
+ }
4806
+ };
4807
+
4718
4808
  const resetTurnStreamState = (rt: SessionRuntime): void => {
4719
4809
  rt.currentTurnText = undefined;
4720
4810
  rt.preAskFlushedText = undefined;
@@ -4851,14 +4941,28 @@ export function createNotificationsExtension(
4851
4941
  });
4852
4942
  else {
4853
4943
  rt.notePromptReconciliation(correlation, { type: "agent_end" });
4854
- if (rt.recordPromptTerminal(correlation))
4855
- rt.emitPromptLifecycle(correlation, { type: "agent_end", sessionId: id, ...correlation });
4944
+ if (rt.recordPromptTerminal(correlation)) {
4945
+ const finalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
4946
+ message =>
4947
+ message &&
4948
+ typeof message === "object" &&
4949
+ (message as { role?: unknown }).role === "assistant" &&
4950
+ (message as { stopReason?: unknown }).stopReason === "stop",
4951
+ );
4952
+ const finalText = finalAssistant ? acpFinalTextFromMessage(finalAssistant).text : "";
4953
+ rt.emitPromptLifecycle(correlation, {
4954
+ type: "agent_end",
4955
+ sessionId: id,
4956
+ ...correlation,
4957
+ ...(finalText ? { finalText } : {}),
4958
+ });
4959
+ }
4856
4960
  }
4857
4961
  } else {
4858
4962
  rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
4859
4963
  }
4860
4964
  rt.activePromptCorrelation = undefined;
4861
- terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "unknown");
4965
+ terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
4862
4966
  try {
4863
4967
  pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
4864
4968
  } catch (e) {
@@ -4976,14 +5080,13 @@ export function createNotificationsExtension(
4976
5080
  // path and ordered before it — unlike message_update, which is queued async),
4977
5081
  // then flushed here before the ask tool's execute calls registerAsk.
4978
5082
  api.on("tool_execution_start", (event, ctx) => {
5083
+ const id = sessionId(ctx);
5084
+ const rt = runtimes.get(id);
5085
+ rt?.emitPromptEvent(event);
4979
5086
  if (event.toolName === "ask") {
4980
- const id = sessionId(ctx);
4981
- const rt = runtimes.get(id);
4982
5087
  if (!rt?.notificationsActive || rt.redact) return;
4983
5088
  flushTurnText(rt, id, rt.currentTurnText, false);
4984
5089
  }
4985
- const id = sessionId(ctx);
4986
- const rt = runtimes.get(id);
4987
5090
  if (!rt?.notificationsActive || rt.redact) return;
4988
5091
  rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
4989
5092
  try {
@@ -4999,12 +5102,32 @@ export function createNotificationsExtension(
4999
5102
  }
5000
5103
  });
5001
5104
 
5105
+ api.on("tool_execution_update", (event, ctx) => {
5106
+ const rt = runtimes.get(sessionId(ctx));
5107
+ rt?.emitPromptEvent(event);
5108
+ });
5109
+
5002
5110
  api.on("tool_execution_end", (event, ctx) => {
5003
5111
  const id = sessionId(ctx);
5004
5112
  const rt = runtimes.get(id);
5005
5113
  if (!rt) return;
5114
+ rt.emitPromptEvent(event);
5006
5115
  const inFlight = rt.inFlightTools.get(event.toolCallId);
5007
- if (!rt.notificationsActive || rt.redact) {
5116
+ if (!inFlight) return;
5117
+ if (!rt.notificationsActive) {
5118
+ rt.inFlightTools.delete(event.toolCallId);
5119
+ return;
5120
+ }
5121
+ if (rt.policySuspended) {
5122
+ if (!inFlight.pendingPhase) {
5123
+ rt.inFlightTools.set(event.toolCallId, {
5124
+ toolName: inFlight.toolName,
5125
+ pendingPhase: event.isError ? "failed" : "completed",
5126
+ });
5127
+ }
5128
+ return;
5129
+ }
5130
+ if (rt.redact) {
5008
5131
  rt.inFlightTools.delete(event.toolCallId);
5009
5132
  return;
5010
5133
  }
@@ -5172,10 +5295,22 @@ export function createNotificationsExtension(
5172
5295
  await Promise.allSettled([...branchStartupTasks]);
5173
5296
  const id = sessionId(ctx);
5174
5297
  const rt = runtimes.get(id);
5175
- if (rt) terminalizeInFlightTools(rt, id, "unknown");
5298
+ if (rt) terminalizeInFlightTools(rt, id, "cancelled");
5299
+ // Startup is only genuinely in flight when a `sessionStartPromises` entry
5300
+ // exists. Once startup has settled, the host is broker-visible and its
5301
+ // post-start `reconcileCurrentSession` may already have minted a
5302
+ // replacement notification-root token whose unregister is still awaiting
5303
+ // its file lock and atomic registry write. Returning before that settles
5304
+ // leaves a stale `sessions[id]` row that the retained older token is
5305
+ // correctly fenced from removing, so shutdown must join it.
5306
+ const startupWasPending = sessionStartPromises.has(id);
5176
5307
  const controllerStop =
5177
5308
  typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
5178
- void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
5309
+ const settledControllerStop = controllerStop.catch(error => {
5310
+ logger.warn(`notifications: controller shutdown failed: ${String(error)}`);
5311
+ return false;
5312
+ });
5313
+ if (startupWasPending) void settledControllerStop;
5179
5314
  try {
5180
5315
  await stopSession(id);
5181
5316
  } catch (error) {
@@ -5186,5 +5321,10 @@ export function createNotificationsExtension(
5186
5321
  // error severity (matching the postmortem cleanup precedent).
5187
5322
  logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
5188
5323
  }
5324
+ // Keep shutdown nonblocking only while native startup is genuinely
5325
+ // pending (the `/notify on` path); otherwise await the controller queue so
5326
+ // completed-start reconciliation and its replacement-token cleanup are
5327
+ // joined before lifecycle shutdown returns.
5328
+ if (!startupWasPending) await settledControllerStop;
5189
5329
  });
5190
5330
  }
@@ -116,6 +116,14 @@ export class RateLimitPool<T = unknown> {
116
116
  return total;
117
117
  }
118
118
 
119
+ /** Whether any queued item currently matches the predicate. */
120
+ someQueued(predicate: (item: RateLimitItem<T>) => boolean): boolean {
121
+ for (const queue of this.lanes.values()) {
122
+ if (queue.some(entry => predicate(entry.item))) return true;
123
+ }
124
+ return false;
125
+ }
126
+
119
127
  /** Current available token count (after refill at `now`). */
120
128
  availableTokens(nowMs: number = this.now()): number {
121
129
  this.refill(nowMs);
@@ -39,12 +39,14 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
39
39
  * Generation 28 rejects special files before retained native authority opens.
40
40
  * Generation 29 adds serving-epoch compatibility, sidecar heartbeat, root GC,
41
41
  * and Bot API cooldown structural fixes (#2956, #2960, #3048).
42
+ * Generation 30 adds opt-in tool activity delivery, closed lifecycle phases,
43
+ * and capability-versioned mixed-host compatibility.
42
44
  */
43
- export const DAEMON_GENERATION = 29;
45
+ export const DAEMON_GENERATION = 30;
44
46
 
45
47
  /**
46
48
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
47
- * all builds published before this field existed; bump this to force serving
48
- * convergence and reload of compatible live predecessors.
49
+ * all builds published before this field existed; epoch 2 covered generation 29;
50
+ * bump this to force serving convergence and reload of compatible live predecessors.
49
51
  */
50
- export const SERVING_EPOCH = 2;
52
+ export const SERVING_EPOCH = 3;