@gajae-code/coding-agent 0.12.6 → 0.12.8

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 (150) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/config/file-lock.d.ts +3 -0
  5. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  6. package/dist/types/config/model-registry.d.ts +2 -0
  7. package/dist/types/config/resolve-config-value.d.ts +2 -1
  8. package/dist/types/config/settings-schema.d.ts +17 -1
  9. package/dist/types/eval/backend.d.ts +2 -2
  10. package/dist/types/exec/bash-executor.d.ts +8 -10
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  12. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  15. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  17. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  18. package/dist/types/sdk/broker/broker.d.ts +5 -1
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
  21. package/dist/types/sdk/bus/config.d.ts +75 -113
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/host.d.ts +2 -23
  33. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  34. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  35. package/dist/types/sdk/index.d.ts +1 -0
  36. package/dist/types/sdk/providers.d.ts +22 -0
  37. package/dist/types/sdk/session-directory.d.ts +1 -1
  38. package/dist/types/session/artifacts.d.ts +5 -8
  39. package/dist/types/session/client-bridge.d.ts +0 -14
  40. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  41. package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
  42. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  43. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  44. package/dist/types/session/messages.d.ts +1 -4
  45. package/dist/types/session/session-manager.d.ts +7 -6
  46. package/dist/types/session/session-storage.d.ts +2 -0
  47. package/dist/types/session/streaming-output.d.ts +0 -32
  48. package/dist/types/task/receipt.d.ts +1 -2
  49. package/dist/types/task/types.d.ts +43 -178
  50. package/dist/types/tools/bash.d.ts +1 -1
  51. package/dist/types/tools/fetch.d.ts +0 -3
  52. package/dist/types/tools/output-meta.d.ts +5 -37
  53. package/dist/types/tools/subagent.d.ts +15 -0
  54. package/dist/types/tools/todo-write.d.ts +3 -0
  55. package/dist/types/tools/tool-result.d.ts +0 -1
  56. package/package.json +10 -8
  57. package/scripts/build-sdk-package-smoke.ts +51 -0
  58. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  59. package/src/async/job-manager.ts +430 -46
  60. package/src/cli/notify-cli.ts +204 -94
  61. package/src/commands/notify.ts +19 -8
  62. package/src/config/file-lock.ts +6 -0
  63. package/src/config/model-discovery-manager.ts +50 -8
  64. package/src/config/model-registry.ts +862 -75
  65. package/src/config/resolve-config-value.ts +10 -8
  66. package/src/config/settings-schema.ts +25 -15
  67. package/src/config/settings.ts +15 -15
  68. package/src/cursor.ts +13 -20
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  70. package/src/eval/backend.ts +2 -2
  71. package/src/exec/bash-executor.ts +151 -425
  72. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  73. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  74. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  75. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  76. package/src/internal-urls/agent-protocol.ts +38 -98
  77. package/src/internal-urls/artifact-protocol.ts +17 -28
  78. package/src/internal-urls/docs-index.generated.ts +7 -7
  79. package/src/internal-urls/registry-helpers.ts +0 -62
  80. package/src/main.ts +43 -7
  81. package/src/modes/DESIGN.md +11 -11
  82. package/src/modes/acp/acp-agent.ts +0 -24
  83. package/src/modes/acp/acp-client-bridge.ts +11 -154
  84. package/src/modes/components/hook-selector.ts +155 -44
  85. package/src/modes/components/model-selector.ts +242 -49
  86. package/src/modes/components/notifications-settings-editor.ts +631 -85
  87. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  88. package/src/modes/controllers/selector-controller.ts +464 -50
  89. package/src/prompts/tools/bash.md +2 -2
  90. package/src/sdk/acp/adapter.ts +9 -200
  91. package/src/sdk/broker/broker.ts +54 -10
  92. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  93. package/src/sdk/broker/lifecycle.ts +47 -50
  94. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  95. package/src/sdk/bus/chat-daemon-control.ts +35 -8
  96. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  97. package/src/sdk/bus/config.ts +534 -286
  98. package/src/sdk/bus/discord-live-provider.ts +57 -2
  99. package/src/sdk/bus/discord-provider.ts +21 -0
  100. package/src/sdk/bus/index.ts +105 -359
  101. package/src/sdk/bus/notification-orchestration.ts +371 -68
  102. package/src/sdk/bus/notification-service.ts +431 -130
  103. package/src/sdk/bus/session-control.ts +119 -44
  104. package/src/sdk/bus/slack-live-provider.ts +78 -2
  105. package/src/sdk/bus/slack-provider.ts +24 -0
  106. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  107. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  108. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  109. package/src/sdk/bus/telegram-daemon.ts +23 -15
  110. package/src/sdk/client/discovery.ts +10 -4
  111. package/src/sdk/host/host.ts +9 -187
  112. package/src/sdk/host/query/handlers.ts +20 -1
  113. package/src/sdk/host/reverse-leases.ts +42 -249
  114. package/src/sdk/index.ts +1 -0
  115. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  116. package/src/sdk/protocol/operation-registry.ts +5 -2
  117. package/src/sdk/providers.ts +51 -0
  118. package/src/sdk/session-directory.ts +1 -0
  119. package/src/sdk/session.ts +10 -7
  120. package/src/session/agent-session.ts +59 -202
  121. package/src/session/artifacts.ts +74 -211
  122. package/src/session/client-bridge.ts +0 -15
  123. package/src/session/internal/managed-session-scope.ts +981 -450
  124. package/src/session/internal/managed-session-storage.ts +362 -124
  125. package/src/session/internal/native-publish-outcome.ts +3 -1
  126. package/src/session/internal/session-open-errors.ts +11 -0
  127. package/src/session/messages.ts +7 -32
  128. package/src/session/session-manager.ts +48 -15
  129. package/src/session/session-storage.ts +72 -5
  130. package/src/session/streaming-output.ts +40 -198
  131. package/src/slash-commands/builtin-registry.ts +82 -4
  132. package/src/task/index.ts +237 -65
  133. package/src/task/receipt.ts +2 -4
  134. package/src/task/render.ts +0 -30
  135. package/src/task/types.ts +10 -34
  136. package/src/tools/ask.ts +1 -1
  137. package/src/tools/bash.ts +239 -818
  138. package/src/tools/eval.ts +4 -99
  139. package/src/tools/fetch.ts +15 -77
  140. package/src/tools/find.ts +3 -7
  141. package/src/tools/output-meta.ts +114 -696
  142. package/src/tools/search.ts +4 -9
  143. package/src/tools/subagent.ts +97 -75
  144. package/src/tools/telegram-send.ts +4 -2
  145. package/src/tools/todo-write.ts +47 -0
  146. package/src/tools/tool-result.ts +0 -1
  147. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  148. package/dist/types/utils/artifact-id.d.ts +0 -2
  149. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  150. package/src/utils/artifact-id.ts +0 -6
@@ -61,12 +61,13 @@ import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "..
61
61
  import { acpFinalTextFromMessage } from "../acp/final-text";
62
62
  import { ensureBroker } from "../broker/ensure";
63
63
  import { SessionIndex } from "../broker/session-index";
64
- import { ReverseLeaseError, SessionSdkHost, shouldHostSdk } from "../host";
64
+ import { SessionSdkHost, shouldHostSdk } from "../host";
65
65
  import { type ControlSurface, dispatchControl } from "../host/control";
66
66
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
67
67
  import { projectQ10Models } from "../models.js";
68
68
  import { PROMPT_CLIENT_REF_MAX_LENGTH, type SdkPromptTerminalOutcome } from "../prompt-status";
69
69
  import { OPERATIONS } from "../protocol/operation-registry";
70
+ import { ActiveProviderResolutionError } from "../providers.js";
70
71
  import {
71
72
  lifecycleStartupCapabilityForApi,
72
73
  normalizeSdkStartupFailure,
@@ -75,13 +76,12 @@ import {
75
76
  import { registerTelegramFileSink } from "./attachment-registry";
76
77
  import { ensureDiscordDaemon, ensureSlackDaemon } from "./chat-daemon-control";
77
78
  import {
79
+ getCurrentTelegramActivationMarker,
78
80
  getNotificationConfig,
79
- isDiscordConfigured,
80
- isSessionNotificationsEnabled,
81
- isSlackConfigured,
82
- isTelegramConfigured,
81
+ isProviderEffectivelyEnabled,
83
82
  type NotificationConfig,
84
83
  type NotificationSettingsReader,
84
+ resolveGenericNotificationSessionEligibility,
85
85
  sessionTag,
86
86
  } from "./config";
87
87
  import { telegramControlCommandUsage } from "./config-commands";
@@ -93,6 +93,7 @@ import {
93
93
  import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
94
94
  import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
95
95
  import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
96
+ import { proposedTelegramIdentity } from "./notification-orchestration";
96
97
  import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
97
98
  import { createReconciliationStore } from "./reconciliation-store";
98
99
  import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
@@ -987,7 +988,7 @@ interface SessionRuntime {
987
988
  serverStopped: boolean;
988
989
  brokerRegistrationReleased: boolean;
989
990
  /** Managed Telegram root registration released during terminal teardown. */
990
- notificationRootRegistration?: { settings: Settings; cwd: string; registrationToken?: string };
991
+ notificationRootRegistration?: { settings: Settings; cwd: string; registrationToken: string };
991
992
  verbosity: "lean" | "verbose";
992
993
  sessionTag: string;
993
994
  /** Whether the agent loop is currently running (drives the typing indicator). */
@@ -2101,6 +2102,13 @@ function sdkQuerySurface(
2101
2102
  promptTerminalOutcomeVersion: 1,
2102
2103
  }),
2103
2104
  getAuthProviders: () => [...new Set(ctx.modelRegistry.getAll().map(model => model.provider))],
2105
+ getActiveProviders: () => {
2106
+ try {
2107
+ return ctx.modelRegistry.getActiveProviders();
2108
+ } catch {
2109
+ throw new ActiveProviderResolutionError();
2110
+ }
2111
+ },
2104
2112
  getTools: () => {
2105
2113
  const tools = typeof (ctx as Partial<ExtensionContext>).getAllTools === "function" ? ctx.getAllTools() : [];
2106
2114
  return tools.length > 0 ? tools : (getInstalledDefinitions("host_tools") ?? []);
@@ -3193,8 +3201,8 @@ export async function ensureConfiguredProviderDaemons(
3193
3201
  configuredSettings,
3194
3202
  ) => (provider === "discord" ? ensureDiscordDaemon(configuredSettings) : ensureSlackDaemon(configuredSettings)),
3195
3203
  ): Promise<void> {
3196
- if (isDiscordConfigured(cfg)) await ensureProviderDaemon("discord", settings);
3197
- if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
3204
+ if (isProviderEffectivelyEnabled(cfg, "discord")) await ensureProviderDaemon("discord", settings);
3205
+ if (isProviderEffectivelyEnabled(cfg, "slack")) await ensureProviderDaemon("slack", settings);
3198
3206
  }
3199
3207
 
3200
3208
  /**
@@ -3241,12 +3249,14 @@ export function createNotificationsExtension(
3241
3249
  new NotificationSessionController({
3242
3250
  eligible: true,
3243
3251
  getConfig: () => resolveSettings(options.settings).cfg,
3252
+ spawnedByGjc: options.spawnedByGjc,
3244
3253
  });
3245
3254
 
3246
3255
  // Failed terminal teardown remains fenced from normal runtime lookup while the
3247
3256
  // exact runtime object retains authority for an explicit idempotent retry.
3248
3257
  const cleanupRetries = new Map<string, SessionRuntime>();
3249
3258
  const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
3259
+ const forceIsolatedChatSessions = new Set<string>();
3250
3260
  const branchStartupTasks = new Set<Promise<void>>();
3251
3261
  const sessionLifecycleTasks = new Set<Promise<void>>();
3252
3262
  let activeRuntimeId: string | undefined;
@@ -3258,8 +3268,6 @@ export function createNotificationsExtension(
3258
3268
  awaitStartup: boolean;
3259
3269
  }
3260
3270
  | undefined;
3261
- const MAX_SYNTHETIC_SEAM_ADMISSIONS = 64;
3262
- let syntheticSeamAdmissions = 0;
3263
3271
  let extensionShuttingDown = false;
3264
3272
 
3265
3273
  async function ensureTelegramOwner(
@@ -3287,18 +3295,34 @@ export function createNotificationsExtension(
3287
3295
  ? "blocked_identity"
3288
3296
  : "ready";
3289
3297
  }
3298
+ type ConfiguredDaemonOwnerResult = "ready" | "blocked_identity" | "blocked_identity_with_sibling";
3290
3299
  async function ensureConfiguredDaemonOwners(
3291
3300
  settings: Settings,
3292
3301
  cfg: NotificationConfig,
3293
3302
  cwd: string,
3294
3303
  id: string,
3295
3304
  onRegistered?: (registrationToken: string) => void,
3296
- ): Promise<boolean> {
3297
- if (isTelegramConfigured(cfg)) {
3298
- if ((await ensureTelegramOwner(settings, cwd, id, onRegistered)) === "blocked_identity") return false;
3305
+ ): Promise<ConfiguredDaemonOwnerResult> {
3306
+ if (isProviderEffectivelyEnabled(cfg, "telegram")) {
3307
+ const telegramMarker = getCurrentTelegramActivationMarker(cfg);
3308
+ if (telegramMarker) {
3309
+ if (!isProviderEffectivelyEnabled(cfg, "discord") && !isProviderEffectivelyEnabled(cfg, "slack")) {
3310
+ return "blocked_identity";
3311
+ }
3312
+ await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
3313
+ return "blocked_identity_with_sibling";
3314
+ }
3315
+ const telegram = await ensureTelegramOwner(settings, cwd, id, onRegistered);
3316
+ if (telegram === "blocked_identity") {
3317
+ if (!isProviderEffectivelyEnabled(cfg, "discord") && !isProviderEffectivelyEnabled(cfg, "slack")) {
3318
+ return "blocked_identity";
3319
+ }
3320
+ await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
3321
+ return "blocked_identity_with_sibling";
3322
+ }
3299
3323
  }
3300
3324
  await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
3301
- return true;
3325
+ return "ready";
3302
3326
  }
3303
3327
  const identityControlOperations = new Set([
3304
3328
  "session.new",
@@ -3486,7 +3510,7 @@ export function createNotificationsExtension(
3486
3510
  const id = sessionId(ctx);
3487
3511
  const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
3488
3512
  const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
3489
- const notificationsEnabledForSession = controller.query(ctx).effectiveEnabled;
3513
+ const notificationsEnabledForSession = controller.query(ctx).genericSessionEnabled;
3490
3514
  const sdkEnabledForSession =
3491
3515
  (options.sdkHostModeSupported ?? true) && shouldHostSdk(settings, isNotificationEligibleContext(ctx));
3492
3516
  const lifecycleRequired = lifecycleStartupCapability !== undefined;
@@ -3527,6 +3551,25 @@ export function createNotificationsExtension(
3527
3551
  }
3528
3552
 
3529
3553
  const stateRoot = path.join(ctx.cwd, ".gjc", "state");
3554
+ let isolateChatEndpoint = forceIsolatedChatSessions.delete(id);
3555
+ if (
3556
+ !isolateChatEndpoint &&
3557
+ notificationsEnabledForSession &&
3558
+ settingsAvailable &&
3559
+ settings &&
3560
+ isProviderEffectivelyEnabled(cfg, "telegram") &&
3561
+ (isProviderEffectivelyEnabled(cfg, "discord") || isProviderEffectivelyEnabled(cfg, "slack")) &&
3562
+ typeof cfg.botToken === "string" &&
3563
+ typeof cfg.chatId === "string"
3564
+ ) {
3565
+ const marker = getCurrentTelegramActivationMarker(cfg);
3566
+ if (marker) isolateChatEndpoint = true;
3567
+ else {
3568
+ const identity = await proposedTelegramIdentity({ settings, botToken: cfg.botToken, chatId: cfg.chatId });
3569
+ isolateChatEndpoint = identity.status === "foreign" || identity.status === "unknown";
3570
+ }
3571
+ }
3572
+ const endpointStateRoot = isolateChatEndpoint ? path.join(stateRoot, "chat") : stateRoot;
3530
3573
  const lifecycleAgentDir = lifecycleRequired ? settings?.getAgentDir?.() : undefined;
3531
3574
  if (lifecycleRequired && !lifecycleAgentDir)
3532
3575
  return failLifecycleStartup("failed", "Lifecycle SDK startup requires an agent directory.");
@@ -3548,7 +3591,7 @@ export function createNotificationsExtension(
3548
3591
  nativeVersion: nativeBuildInfo().version,
3549
3592
  notificationServer: NotificationServer.prototype,
3550
3593
  });
3551
- server = new NotificationServer(id, token, stateRoot, true);
3594
+ server = new NotificationServer(id, token, endpointStateRoot, true);
3552
3595
  } catch (error) {
3553
3596
  if (lifecycleRequired) return failLifecycleStartup("failed", error);
3554
3597
  throw error;
@@ -3569,64 +3612,8 @@ export function createNotificationsExtension(
3569
3612
  const revisions = new RevisionStore(id, Date.now, { storageDir: stateRoot });
3570
3613
  let host: SessionSdkHost | undefined;
3571
3614
  let disposeUiAnswerSource: (() => void) | undefined;
3572
- let fsClientBridge: ClientBridge | undefined;
3573
- let terminalClientBridge: ClientBridge | undefined;
3574
- let terminalPublicationFailedLeaseId: string | undefined;
3575
- const MAX_PENDING_TERMINAL_CREATIONS = 32;
3576
- let pendingTerminalCreations = 0;
3577
- let terminalCreationTail = Promise.resolve();
3578
- const withTerminalCreationAdmission = async <T>(operation: () => Promise<T>): Promise<T> => {
3579
- if (pendingTerminalCreations >= MAX_PENDING_TERMINAL_CREATIONS)
3580
- throw new Error("terminal creation admission limit reached");
3581
- pendingTerminalCreations++;
3582
- const previous = terminalCreationTail;
3583
- const release = Promise.withResolvers<void>();
3584
- terminalCreationTail = previous.then(() => release.promise);
3585
- try {
3586
- await previous;
3587
- return await operation();
3588
- } finally {
3589
- pendingTerminalCreations--;
3590
- release.resolve();
3591
- }
3592
- };
3593
- const providerLeaseIdentities = new Map<
3594
- string,
3595
- { connectionId: string; connectionGeneration: number; fence: string }
3596
- >();
3597
- const refreshSdkClientBridge = () => {
3598
- if (!fsClientBridge && !terminalClientBridge) {
3599
- ctx.setSdkClientBridge?.(undefined);
3600
- return;
3601
- }
3602
- const bridge: ClientBridge = {
3603
- capabilities: {
3604
- readTextFile: fsClientBridge?.capabilities.readTextFile,
3605
- writeTextFile: fsClientBridge?.capabilities.writeTextFile,
3606
- terminal: terminalClientBridge?.capabilities.terminal,
3607
- },
3608
- deferAgentInitiatedTurns: true,
3609
- };
3610
- if (fsClientBridge?.readTextFile) bridge.readTextFile = fsClientBridge.readTextFile;
3611
- if (fsClientBridge?.writeTextFile) bridge.writeTextFile = fsClientBridge.writeTextFile;
3612
- if (terminalClientBridge?.quarantineProviderLease)
3613
- bridge.quarantineProviderLease = terminalClientBridge.quarantineProviderLease;
3614
- if (terminalClientBridge?.createTerminal) bridge.createTerminal = terminalClientBridge.createTerminal;
3615
- ctx.setSdkClientBridge?.(bridge);
3616
- };
3617
- const installProviderDefinitions = (
3618
- capability: string,
3619
- definitions: unknown,
3620
- leaseId: string,
3621
- connectionId: string,
3622
- connectionGeneration: number,
3623
- ) => {
3615
+ const installProviderDefinitions = (capability: string, definitions: unknown) => {
3624
3616
  validateProviderDefinitions(capability, definitions);
3625
- providerLeaseIdentities.set(leaseId, {
3626
- connectionId,
3627
- connectionGeneration,
3628
- fence: `${connectionId}:${connectionGeneration}:${leaseId}`,
3629
- });
3630
3617
  if (capability === "permission") {
3631
3618
  ctx.setSdkPermissionProvider?.(async (toolCall, permissionOptions, signal) => {
3632
3619
  const result = await host!.reverse.request(
@@ -3664,179 +3651,6 @@ export function createNotificationsExtension(
3664
3651
  );
3665
3652
  return;
3666
3653
  }
3667
- if (capability === "terminal") {
3668
- const terminalLeaseId = leaseId;
3669
- if (!terminalLeaseId) throw new Error("terminal provider lease is unavailable");
3670
- const terminalProviderIdentity = providerLeaseIdentities.get(terminalLeaseId);
3671
- if (!terminalProviderIdentity) throw new Error("terminal provider generation is unavailable");
3672
- if (terminalPublicationFailedLeaseId !== terminalLeaseId) terminalPublicationFailedLeaseId = undefined;
3673
- const requestTerminal = (
3674
- method: string,
3675
- payload: Record<string, unknown>,
3676
- signal?: AbortSignal,
3677
- cleanup = false,
3678
- ) =>
3679
- host!.reverse.request(
3680
- "terminal",
3681
- method,
3682
- payload,
3683
- signal,
3684
- terminalLeaseId,
3685
- cleanup,
3686
- terminalProviderIdentity.connectionId,
3687
- );
3688
- const settleTerminalCleanup = async (
3689
- method: "terminal.kill" | "terminal.release",
3690
- terminalId: string,
3691
- ): Promise<boolean> =>
3692
- await Promise.race([
3693
- Promise.resolve()
3694
- .then(() => requestTerminal(method, { terminalId }, AbortSignal.timeout(500), true))
3695
- .then(
3696
- () => true,
3697
- () => false,
3698
- ),
3699
- Bun.sleep(500).then(() => false),
3700
- ]);
3701
- terminalClientBridge = {
3702
- capabilities: { terminal: true },
3703
- deferAgentInitiatedTurns: true,
3704
- async quarantineProviderLease(identity, _reason) {
3705
- if (typeof identity === "string") throw new Error("terminal provider generation is required");
3706
- if (
3707
- identity.leaseId !== terminalLeaseId ||
3708
- identity.connectionId !== terminalProviderIdentity.connectionId ||
3709
- identity.connectionGeneration !== terminalProviderIdentity.connectionGeneration ||
3710
- identity.fence !== terminalProviderIdentity.fence
3711
- )
3712
- throw new Error("terminal provider lease mismatch");
3713
- terminalPublicationFailedLeaseId = terminalLeaseId;
3714
- host!.quarantineProviderLease(identity, "terminal cleanup ownership uncertain");
3715
- },
3716
- async createTerminal(params) {
3717
- return withTerminalCreationAdmission(async () => {
3718
- if (terminalPublicationFailedLeaseId === terminalLeaseId)
3719
- throw new Error("terminal provider quarantined after publication failure");
3720
- if (params.signal?.aborted) throw new Error("terminal creation aborted");
3721
- const creationAbort = new AbortController();
3722
- const creationSignal = params.signal
3723
- ? AbortSignal.any([params.signal, creationAbort.signal])
3724
- : creationAbort.signal;
3725
- let created: unknown;
3726
- let creationTimedOut = false;
3727
- try {
3728
- created = await Promise.race([
3729
- requestTerminal(
3730
- "terminal.create",
3731
- {
3732
- command: params.command,
3733
- ...(params.args ? { args: params.args } : {}),
3734
- ...(params.env ? { env: params.env } : {}),
3735
- ...(params.cwd ? { cwd: params.cwd } : {}),
3736
- ...(typeof params.outputByteLimit === "number"
3737
- ? { outputByteLimit: params.outputByteLimit }
3738
- : {}),
3739
- },
3740
- creationSignal,
3741
- ),
3742
- Bun.sleep(500).then(() => {
3743
- creationTimedOut = true;
3744
- creationAbort.abort();
3745
- throw new Error("terminal creation did not settle within 500ms");
3746
- }),
3747
- ]);
3748
- } catch (error) {
3749
- if (
3750
- creationSignal.aborted ||
3751
- (error instanceof ReverseLeaseError && error.code === "payload_too_large")
3752
- )
3753
- terminalPublicationFailedLeaseId = terminalLeaseId;
3754
- if (creationTimedOut) throw new Error("terminal creation did not settle within 500ms");
3755
- throw error;
3756
- } finally {
3757
- creationAbort.abort();
3758
- }
3759
- const terminalId =
3760
- created &&
3761
- typeof created === "object" &&
3762
- typeof (created as { terminalId?: unknown }).terminalId === "string"
3763
- ? (created as { terminalId: string }).terminalId
3764
- : undefined;
3765
- if (!terminalId) {
3766
- terminalPublicationFailedLeaseId = terminalLeaseId;
3767
- throw new Error("terminal provider returned an invalid create response");
3768
- }
3769
- const publicationAbort = new AbortController();
3770
- const publicationSignal = params.signal
3771
- ? AbortSignal.any([params.signal, publicationAbort.signal])
3772
- : publicationAbort.signal;
3773
- try {
3774
- await Promise.race([
3775
- requestTerminal(
3776
- "terminal.publish",
3777
- { toolCallId: params.toolCallId, terminalId },
3778
- publicationSignal,
3779
- ),
3780
- Bun.sleep(500).then(() => {
3781
- publicationAbort.abort();
3782
- throw new Error("terminal publication did not settle within 500ms");
3783
- }),
3784
- ]);
3785
- } catch (error) {
3786
- const killed = await settleTerminalCleanup("terminal.kill", terminalId);
3787
- const released = await settleTerminalCleanup("terminal.release", terminalId);
3788
- if (!params.signal?.aborted || !killed || !released)
3789
- terminalPublicationFailedLeaseId = terminalLeaseId;
3790
- throw error;
3791
- } finally {
3792
- publicationAbort.abort();
3793
- }
3794
- return {
3795
- terminalId,
3796
- providerLeaseId: terminalLeaseId,
3797
- providerConnectionId: terminalProviderIdentity.connectionId,
3798
- providerConnectionGeneration: terminalProviderIdentity.connectionGeneration,
3799
- providerFence: terminalProviderIdentity.fence,
3800
- async currentOutput() {
3801
- const output = await requestTerminal("terminal.output", { terminalId });
3802
- if (
3803
- !output ||
3804
- typeof output !== "object" ||
3805
- typeof (output as { output?: unknown }).output !== "string" ||
3806
- typeof (output as { truncated?: unknown }).truncated !== "boolean"
3807
- ) {
3808
- throw new Error("terminal provider returned an invalid output response");
3809
- }
3810
- const response = output as { output: string; truncated: boolean; exitStatus?: unknown };
3811
- return {
3812
- output: response.output,
3813
- truncated: response.truncated,
3814
- exitStatus:
3815
- response.exitStatus && typeof response.exitStatus === "object"
3816
- ? (response.exitStatus as { exitCode?: number | null; signal?: string | null })
3817
- : null,
3818
- };
3819
- },
3820
- async waitForExit() {
3821
- const status = await requestTerminal("terminal.waitForExit", { terminalId });
3822
- if (!status || typeof status !== "object")
3823
- throw new Error("terminal provider returned an invalid exit response");
3824
- const response = status as { exitCode?: number | null; signal?: string | null };
3825
- return { exitCode: response.exitCode ?? null, signal: response.signal ?? null };
3826
- },
3827
- async kill() {
3828
- await requestTerminal("terminal.kill", { terminalId }, undefined, true);
3829
- },
3830
- async release() {
3831
- await requestTerminal("terminal.release", { terminalId }, undefined, true);
3832
- },
3833
- };
3834
- });
3835
- },
3836
- };
3837
- refreshSdkClientBridge();
3838
- return;
3839
- }
3840
3654
  if (capability !== "fs") return;
3841
3655
  // Only advertise the methods the client actually declared; a read-only client
3842
3656
  // must keep using the local write path instead of failing against the bridge.
@@ -3868,17 +3682,11 @@ export function createNotificationsExtension(
3868
3682
  };
3869
3683
  if (!canRead) delete bridge.readTextFile;
3870
3684
  if (!canWrite) delete bridge.writeTextFile;
3871
- fsClientBridge = bridge;
3872
- refreshSdkClientBridge();
3685
+ ctx.setSdkClientBridge?.(bridge);
3873
3686
  };
3874
3687
  const removeProviderDefinitions = (capability: string) => {
3875
3688
  if (capability === "permission") ctx.setSdkPermissionProvider?.(undefined);
3876
- if (capability === "fs") fsClientBridge = undefined;
3877
- if (capability === "terminal") {
3878
- terminalClientBridge = undefined;
3879
- terminalPublicationFailedLeaseId = undefined;
3880
- }
3881
- if (capability === "fs" || capability === "terminal") refreshSdkClientBridge();
3689
+ if (capability === "fs") ctx.setSdkClientBridge?.(undefined);
3882
3690
  if (capability === "ui") {
3883
3691
  disposeUiAnswerSource?.();
3884
3692
  disposeUiAnswerSource = undefined;
@@ -3886,12 +3694,6 @@ export function createNotificationsExtension(
3886
3694
  };
3887
3695
 
3888
3696
  const hostCapCache = new Map<string, ReadonlySet<string>>();
3889
- const MAX_PENDING_PROVIDER_RECLAIMS = 64;
3890
- const pendingProviderReclaims = new Map<
3891
- string,
3892
- Array<{ connectionId: string; connectionGeneration: number; frame: Record<string, unknown> }>
3893
- >();
3894
- let pendingProviderReclaimCount = 0;
3895
3697
 
3896
3698
  const configOverrides = new Map<string, unknown>();
3897
3699
  const configRevision = { current: 0 };
@@ -4445,8 +4247,7 @@ export function createNotificationsExtension(
4445
4247
  };
4446
4248
 
4447
4249
  const sendSdkFrame = (connectionId: string, frame: Record<string, unknown>) => {
4448
- const cancellation = frame.type === "reverse_cancel";
4449
- if (!cancellation && (extensionShuttingDown || runtime?.stopping || runtimes.get(id) !== runtime)) {
4250
+ if (extensionShuttingDown || runtime?.stopping || runtimes.get(id) !== runtime) {
4450
4251
  abandonPromptResponse(connectionId, frame);
4451
4252
  return;
4452
4253
  }
@@ -4465,15 +4266,13 @@ export function createNotificationsExtension(
4465
4266
  abandonPromptResponse(connectionId, frame);
4466
4267
  throw error;
4467
4268
  }
4468
- syntheticSeamAdmissions = Math.max(0, syntheticSeamAdmissions - 1);
4469
- host?.handleDisconnect(connectionId);
4470
4269
  return;
4471
4270
  }
4472
4271
  try {
4473
4272
  server.sendTo(connectionId, json);
4474
4273
  } catch (error) {
4274
+ logger.warn(`sdk: directed response delivery failed for ${connectionId}: ${String(error)}`);
4475
4275
  abandonPromptResponse(connectionId, frame);
4476
- logger.warn(`sdk: directed response delivery rejected for ${connectionId}: ${String(error)}`);
4477
4276
  throw error;
4478
4277
  }
4479
4278
  };
@@ -4486,13 +4285,6 @@ export function createNotificationsExtension(
4486
4285
  connectionCapabilities: connectionId => hostCapCache.get(connectionId),
4487
4286
  installProviderDefinitions,
4488
4287
  onProviderDefinitionsRemoved: removeProviderDefinitions,
4489
- onProviderLeaseRegistered: (lease, connectionGeneration) => {
4490
- providerLeaseIdentities.set(lease.leaseId, {
4491
- connectionId: lease.connectionId,
4492
- connectionGeneration,
4493
- fence: `${lease.connectionId}:${connectionGeneration}:${lease.leaseId}`,
4494
- });
4495
- },
4496
4288
  onFrame: handler => {
4497
4289
  inboundSdkFrame = handler;
4498
4290
  return () => {
@@ -4500,11 +4292,6 @@ export function createNotificationsExtension(
4500
4292
  };
4501
4293
  },
4502
4294
  onRequest: options.onSdkRequest,
4503
- onFrameOverflow: connectionId => {
4504
- fencedConnections.add(connectionId);
4505
- controlSurface.cancelPendingPreflightsForConnection(connectionId);
4506
- host?.handleDisconnect(connectionId);
4507
- },
4508
4295
  beforeControlResponse: async (_connectionId, request, response, sendTerminal) => {
4509
4296
  if (typeof request.operation !== "string" || !identityControlOperations.has(request.operation)) return;
4510
4297
  const pending = deferredIdentityRotation;
@@ -4796,47 +4583,6 @@ export function createNotificationsExtension(
4796
4583
  server.sendTo(connectionId, JSON.stringify({ type: responseType, id, ok: false, error }));
4797
4584
  } catch {}
4798
4585
  };
4799
- const queueExpectedProviderReclaim = (connectionId: string, frame: Record<string, unknown>): boolean => {
4800
- if (frame.type !== "register_provider") return false;
4801
- const capability = typeof frame.capability === "string" ? frame.capability : undefined;
4802
- const expectedLeaseId = typeof frame.expectedLeaseId === "string" ? frame.expectedLeaseId : undefined;
4803
- if (!capability || !expectedLeaseId) return false;
4804
- const lease = host?.reverse.getLease(capability);
4805
- if (!lease || lease.connectionId === connectionId || lease.leaseId !== expectedLeaseId) return false;
4806
- if (pendingProviderReclaimCount >= MAX_PENDING_PROVIDER_RECLAIMS) return false;
4807
- const queued = pendingProviderReclaims.get(lease.connectionId) ?? [];
4808
- if (typeof frame.id === "string" && queued.some(entry => entry.frame.id === frame.id)) return true;
4809
- const currentGeneration = host?.connectionGeneration(connectionId) ?? 0;
4810
- if (currentGeneration <= 0) return false;
4811
- queued.push({ connectionId, connectionGeneration: currentGeneration, frame });
4812
- pendingProviderReclaims.set(lease.connectionId, queued);
4813
- pendingProviderReclaimCount++;
4814
- return true;
4815
- };
4816
- const purgeProviderReclaimsForClaimant = (connectionId: string): void => {
4817
- for (const [incumbentId, queued] of pendingProviderReclaims) {
4818
- const retained = queued.filter(entry => entry.connectionId !== connectionId);
4819
- pendingProviderReclaimCount = Math.max(0, pendingProviderReclaimCount - (queued.length - retained.length));
4820
- if (retained.length === 0) pendingProviderReclaims.delete(incumbentId);
4821
- else pendingProviderReclaims.set(incumbentId, retained);
4822
- }
4823
- };
4824
- const drainProviderReclaims = (closedConnectionId: string): void => {
4825
- const queued = pendingProviderReclaims.get(closedConnectionId);
4826
- if (!queued) return;
4827
- pendingProviderReclaims.delete(closedConnectionId);
4828
- pendingProviderReclaimCount = Math.max(0, pendingProviderReclaimCount - queued.length);
4829
- for (const entry of queued) {
4830
- if (
4831
- initializedRuntime.inboundFenced ||
4832
- initializedRuntime.stopping ||
4833
- runtimes.get(id) !== initializedRuntime
4834
- )
4835
- continue;
4836
- if (!host?.isConnectionOpen(entry.connectionId, entry.connectionGeneration)) continue;
4837
- inboundSdkFrame?.(entry.connectionId, entry.frame);
4838
- }
4839
- };
4840
4586
  try {
4841
4587
  server.onSdkFrame((err, inbound) => {
4842
4588
  if (err || !inbound) return;
@@ -4844,13 +4590,6 @@ export function createNotificationsExtension(
4844
4590
  const frame = JSON.parse(inbound.json) as unknown;
4845
4591
  if (!frame || typeof frame !== "object") return;
4846
4592
  const typedFrame = frame as Record<string, unknown>;
4847
- if (!host.isConnectionOpen(inbound.connectionId)) {
4848
- if (host.connectionGeneration(inbound.connectionId) > 0) {
4849
- sendEndpointStale(inbound.connectionId, typedFrame);
4850
- return;
4851
- }
4852
- host.handleConnect(inbound.connectionId);
4853
- }
4854
4593
  if (inbound.connectionId && fencedConnections.has(inbound.connectionId)) {
4855
4594
  sendEndpointStale(inbound.connectionId, typedFrame);
4856
4595
  return;
@@ -4859,7 +4598,6 @@ export function createNotificationsExtension(
4859
4598
  sendEndpointStale(inbound.connectionId, typedFrame);
4860
4599
  return;
4861
4600
  }
4862
- if (queueExpectedProviderReclaim(inbound.connectionId, typedFrame)) return;
4863
4601
  if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
4864
4602
  if (typedFrame.type === "event_replay") {
4865
4603
  const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
@@ -4887,16 +4625,11 @@ export function createNotificationsExtension(
4887
4625
  server.onConnectionClose((_err, connectionId) => {
4888
4626
  if (!connectionId) return;
4889
4627
  controlSurface.cancelPendingPreflightsForConnection(connectionId);
4890
- try {
4891
- host.handleDisconnect(connectionId);
4892
- } finally {
4893
- drainProviderReclaims(connectionId);
4894
- }
4628
+ host.handleDisconnect(connectionId);
4895
4629
  hostCapCache.delete(connectionId);
4896
4630
  // The socket is gone, so its fence has nothing left to refuse. Dropping the
4897
4631
  // entry keeps the set bounded by live connections instead of growing forever.
4898
4632
  fencedConnections.delete(connectionId);
4899
- purgeProviderReclaimsForClaimant(connectionId);
4900
4633
  // Deliberate deviation from the plan's "claim prompt_failed on old-owner
4901
4634
  // disconnect": that would break the shipped Q26 reconnect contract, where a
4902
4635
  // client may drop its socket and reconcile the still-running prompt without
@@ -5177,9 +4910,7 @@ export function createNotificationsExtension(
5177
4910
  !suspendedRuntime.stopping &&
5178
4911
  !suspendedRuntime.policySuspended
5179
4912
  )
5180
- if (syntheticSeamAdmissions >= MAX_SYNTHETIC_SEAM_ADMISSIONS) return;
5181
- syntheticSeamAdmissions++;
5182
- inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
4913
+ inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
5183
4914
  });
5184
4915
  }
5185
4916
  }
@@ -5188,8 +4919,6 @@ export function createNotificationsExtension(
5188
4919
  if (inbound.kind === "control_command") {
5189
4920
  const frame = sdkInboundFrame(inbound.commandJson);
5190
4921
  if (frame) {
5191
- if (syntheticSeamAdmissions >= MAX_SYNTHETIC_SEAM_ADMISSIONS) return;
5192
- syntheticSeamAdmissions++;
5193
4922
  inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
5194
4923
  return;
5195
4924
  }
@@ -5322,18 +5051,26 @@ export function createNotificationsExtension(
5322
5051
  if (notificationsEnabledForSession && settingsAvailable && settings) {
5323
5052
  try {
5324
5053
  let registrationToken: string | undefined;
5325
- if (
5326
- !(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
5327
- registrationToken = token;
5328
- }))
5329
- ) {
5054
+ const ownership = await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
5055
+ registrationToken = token;
5056
+ });
5057
+ if (ownership === "blocked_identity") {
5330
5058
  const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
5331
5059
  finishStartup(result);
5332
5060
  await cleanupAbandonedStartup();
5333
5061
  return result;
5334
5062
  }
5335
- if (isTelegramConfigured(cfg))
5063
+ if (ownership === "blocked_identity_with_sibling" && !isolateChatEndpoint) {
5064
+ await cleanupAbandonedStartup();
5065
+ if (sessionStartPromises.get(id) === startSettled.promise) sessionStartPromises.delete(id);
5066
+ forceIsolatedChatSessions.add(id);
5067
+ const result = await startSession(ctx);
5068
+ finishStartup(result);
5069
+ return result;
5070
+ }
5071
+ if (registrationToken !== undefined) {
5336
5072
  runtime.notificationRootRegistration = { settings, cwd: ctx.cwd, registrationToken };
5073
+ }
5337
5074
  } catch (error) {
5338
5075
  const result = failLifecycleStartup("failed", error);
5339
5076
  finishStartup(result);
@@ -5375,8 +5112,8 @@ export function createNotificationsExtension(
5375
5112
  throwIfLifecycleStopped();
5376
5113
  const index = await new SessionIndex(agentDir).open();
5377
5114
  throwIfLifecycleStopped();
5378
- const locator = { repo: path.resolve(ctx.cwd), stateRoot };
5379
- const endpointMtimeMs = fs.statSync(path.join(stateRoot, "sdk", `${id}.json`)).mtimeMs;
5115
+ const locator = { repo: path.resolve(ctx.cwd), stateRoot: endpointStateRoot };
5116
+ const endpointMtimeMs = fs.statSync(path.join(endpointStateRoot, "sdk", `${id}.json`)).mtimeMs;
5380
5117
  await host.registerWithBroker({
5381
5118
  // The endpoint is written before registration. Its exact mtime
5382
5119
  // binds this index generation to that discovery record.
@@ -5610,6 +5347,19 @@ export function createNotificationsExtension(
5610
5347
  return result.status === "started" || result.status === "already" ? "started" : result.status;
5611
5348
  },
5612
5349
  stop: async binding => await stopSession(binding.sessionId, "notifications"),
5350
+ isolateTelegram: async binding => {
5351
+ const runtime = runtimes.get(binding.sessionId);
5352
+ if (runtime) {
5353
+ const stopped = await stopSession(binding.sessionId, "session", runtime);
5354
+ if (!stopped || runtimes.has(binding.sessionId) || cleanupRetries.has(binding.sessionId)) return "failed";
5355
+ }
5356
+ forceIsolatedChatSessions.add(binding.sessionId);
5357
+ const result = await startSession(binding.context);
5358
+ if (result.status !== "started" && result.status !== "already") {
5359
+ forceIsolatedChatSessions.delete(binding.sessionId);
5360
+ }
5361
+ return result.status === "started" || result.status === "already" ? "started" : result.status;
5362
+ },
5613
5363
  refreshPolicy: (binding, policy) => {
5614
5364
  const runtime = runtimes.get(binding.sessionId);
5615
5365
  if (!runtime) return;
@@ -5655,8 +5405,8 @@ export function createNotificationsExtension(
5655
5405
  registrationToken = token;
5656
5406
  });
5657
5407
  const runtime = runtimes.get(binding.sessionId);
5658
- const configured = isTelegramConfigured(resolveSettings(options.settings).cfg);
5659
- if (result === "ready" && runtime && !runtime.stopping && configured) {
5408
+ const configured = isProviderEffectivelyEnabled(resolveSettings(options.settings).cfg, "telegram");
5409
+ if (result === "ready" && runtime && !runtime.stopping && configured && registrationToken !== undefined) {
5660
5410
  runtime.notificationRootRegistration = { settings, cwd: binding.cwd, registrationToken };
5661
5411
  } else if (registrationToken !== undefined) {
5662
5412
  await unregisterNotificationRoot({
@@ -5680,11 +5430,14 @@ export function createNotificationsExtension(
5680
5430
  const id = sessionId(ctx);
5681
5431
  const command = args.trim().split(/\s+/, 1)[0]?.toLowerCase() || "status";
5682
5432
  const resolved = resolveSettings(options.settings);
5683
- const enabledWithoutLocalOff = isSessionNotificationsEnabled({
5433
+ const manualEligibilityEnv =
5434
+ process.env.GJC_NOTIFICATIONS === "0" ? { ...process.env, GJC_NOTIFICATIONS: undefined } : process.env;
5435
+ const enabledWithoutLocalOff = resolveGenericNotificationSessionEligibility({
5684
5436
  cfg: resolved.cfg,
5685
- env: process.env,
5437
+ env: manualEligibilityEnv,
5686
5438
  sessionDisabled: false,
5687
- });
5439
+ spawnedByGjc: options.spawnedByGjc,
5440
+ }).enabled;
5688
5441
 
5689
5442
  if (command === "off") {
5690
5443
  const result = await controller.setLocalEnabled(ctx, false);
@@ -5702,13 +5455,6 @@ export function createNotificationsExtension(
5702
5455
  ctx.ui.notify("Notifications are disabled for subagent sessions.", "warning");
5703
5456
  return;
5704
5457
  }
5705
- if (process.env.GJC_NOTIFICATIONS === "0") {
5706
- ctx.ui.notify(
5707
- "Notifications remain disabled: GJC_NOTIFICATIONS=0 is an authoritative opt-out.",
5708
- "warning",
5709
- );
5710
- return;
5711
- }
5712
5458
  if (!enabledWithoutLocalOff) {
5713
5459
  ctx.ui.notify(
5714
5460
  "Notifications are not configured. Run `gjc notify setup` or set GJC_NOTIFICATIONS=1.",
@@ -5717,7 +5463,7 @@ export function createNotificationsExtension(
5717
5463
  return;
5718
5464
  }
5719
5465
  const result = await controller.setLocalEnabled(ctx, true);
5720
- const enabled = result.status.running && result.status.effectiveEnabled;
5466
+ const enabled = result.status.running && result.status.genericSessionEnabled;
5721
5467
  const rotated = sessionId(ctx) !== id;
5722
5468
  if (rotated) await stopSession(id);
5723
5469
  const failed = result.outcome === "failed" || (!enabled && !rotated && activeRuntimeId !== id);
@@ -5742,7 +5488,7 @@ export function createNotificationsExtension(
5742
5488
  const status = controller.query(ctx);
5743
5489
  const runtime = runtimes.get(id);
5744
5490
  ctx.ui.notify(
5745
- `Notifications ${status.running ? "running" : status.effectiveEnabled ? "enabled" : "disabled"} for this session; redaction ${(runtime?.redact ?? resolved.cfg.redact) ? "on" : "off"}; verbosity ${runtime?.verbosity ?? resolved.cfg.verbosity}${status.locallyEnabled ? "" : "; locally off"}.`,
5491
+ `Notifications ${status.running ? "running" : status.genericSessionEnabled ? "enabled" : "disabled"} for this session; admission ${status.genericEligibilitySource}; redaction ${(runtime?.redact ?? resolved.cfg.redact) ? "on" : "off"}; verbosity ${runtime?.verbosity ?? resolved.cfg.verbosity}${status.locallyEnabled ? "" : "; locally off"}.`,
5746
5492
  "info",
5747
5493
  );
5748
5494
  },