@gajae-code/coding-agent 0.12.7 → 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 (95) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/commands/notify.d.ts +4 -0
  3. package/dist/types/config/file-lock.d.ts +3 -0
  4. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/resolve-config-value.d.ts +2 -1
  7. package/dist/types/config/settings-schema.d.ts +17 -1
  8. package/dist/types/main.d.ts +1 -0
  9. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  10. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  11. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  12. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  13. package/dist/types/sdk/broker/broker.d.ts +5 -1
  14. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  16. package/dist/types/sdk/bus/config.d.ts +75 -113
  17. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  18. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  19. package/dist/types/sdk/bus/index.d.ts +1 -1
  20. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  21. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  22. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  23. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  24. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  25. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  26. package/dist/types/sdk/client/discovery.d.ts +2 -1
  27. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  28. package/dist/types/sdk/index.d.ts +1 -0
  29. package/dist/types/sdk/providers.d.ts +22 -0
  30. package/dist/types/sdk/session-directory.d.ts +1 -1
  31. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  32. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  33. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  34. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  35. package/dist/types/session/session-manager.d.ts +3 -1
  36. package/dist/types/session/session-storage.d.ts +2 -0
  37. package/package.json +10 -8
  38. package/scripts/build-sdk-package-smoke.ts +51 -0
  39. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  40. package/src/cli/notify-cli.ts +204 -94
  41. package/src/commands/notify.ts +19 -8
  42. package/src/config/file-lock.ts +6 -0
  43. package/src/config/model-discovery-manager.ts +50 -8
  44. package/src/config/model-registry.ts +862 -75
  45. package/src/config/resolve-config-value.ts +10 -8
  46. package/src/config/settings-schema.ts +25 -15
  47. package/src/config/settings.ts +15 -15
  48. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  49. package/src/internal-urls/docs-index.generated.ts +6 -6
  50. package/src/main.ts +43 -7
  51. package/src/modes/DESIGN.md +11 -11
  52. package/src/modes/components/hook-selector.ts +155 -44
  53. package/src/modes/components/model-selector.ts +242 -49
  54. package/src/modes/components/notifications-settings-editor.ts +631 -85
  55. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  56. package/src/modes/controllers/selector-controller.ts +464 -50
  57. package/src/sdk/broker/broker.ts +54 -10
  58. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  59. package/src/sdk/broker/lifecycle.ts +47 -50
  60. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  61. package/src/sdk/bus/chat-daemon-control.ts +35 -6
  62. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  63. package/src/sdk/bus/config.ts +534 -286
  64. package/src/sdk/bus/discord-live-provider.ts +57 -2
  65. package/src/sdk/bus/discord-provider.ts +21 -0
  66. package/src/sdk/bus/index.ts +97 -35
  67. package/src/sdk/bus/notification-orchestration.ts +371 -68
  68. package/src/sdk/bus/notification-service.ts +431 -130
  69. package/src/sdk/bus/session-control.ts +119 -44
  70. package/src/sdk/bus/slack-live-provider.ts +78 -2
  71. package/src/sdk/bus/slack-provider.ts +24 -0
  72. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  73. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  74. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  75. package/src/sdk/bus/telegram-daemon.ts +23 -15
  76. package/src/sdk/client/discovery.ts +10 -4
  77. package/src/sdk/host/query/handlers.ts +20 -1
  78. package/src/sdk/index.ts +1 -0
  79. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  80. package/src/sdk/protocol/operation-registry.ts +5 -2
  81. package/src/sdk/providers.ts +51 -0
  82. package/src/sdk/session-directory.ts +1 -0
  83. package/src/sdk/session.ts +10 -7
  84. package/src/session/agent-session.ts +44 -24
  85. package/src/session/internal/managed-session-scope.ts +981 -450
  86. package/src/session/internal/managed-session-storage.ts +382 -45
  87. package/src/session/internal/native-publish-outcome.ts +3 -1
  88. package/src/session/internal/session-open-errors.ts +11 -0
  89. package/src/session/session-manager.ts +27 -3
  90. package/src/session/session-storage.ts +72 -5
  91. package/src/slash-commands/builtin-registry.ts +82 -4
  92. package/src/task/index.ts +9 -3
  93. package/src/tools/ask.ts +1 -1
  94. package/src/tools/bash.ts +1 -1
  95. package/src/tools/telegram-send.ts +4 -2
@@ -1,4 +1,12 @@
1
- import type { DiscordInboundEvent, DiscordMessageComponent, DiscordProvider, DiscordThread } from "./discord-provider";
1
+ import type {
2
+ DiscordConfigurationProbeResult,
3
+ DiscordDiagnosticProvider,
4
+ DiscordInboundEvent,
5
+ DiscordMessageComponent,
6
+ DiscordOneShotTestResult,
7
+ DiscordProvider,
8
+ DiscordThread,
9
+ } from "./discord-provider";
2
10
 
3
11
  const API_BASE = "https://discord.com/api/v10";
4
12
  const GATEWAY_INTENTS = 1 + 512 + 32_768;
@@ -35,7 +43,7 @@ export interface DiscordLiveProviderOptions {
35
43
  type JsonRecord = Record<string, unknown>;
36
44
 
37
45
  /** Discord REST/Gateway implementation. The only credential is held privately and is never emitted. */
38
- export class DiscordLiveProvider implements DiscordProvider {
46
+ export class DiscordLiveProvider implements DiscordProvider, DiscordDiagnosticProvider {
39
47
  readonly applicationId: string;
40
48
  readonly #token: string;
41
49
  readonly #fetch: DiscordFetch;
@@ -230,6 +238,53 @@ export class DiscordLiveProvider implements DiscordProvider {
230
238
  return null;
231
239
  }
232
240
 
241
+ async probeConfiguration(signal?: AbortSignal): Promise<DiscordConfigurationProbeResult> {
242
+ if (signal?.aborted) return { ok: false, detail: "Discord configuration probe cancelled." };
243
+ try {
244
+ const currentUser = await this.#request("/users/@me", { signal });
245
+ const botUserId = this.#string(currentUser, "id");
246
+ if (!botUserId) return { ok: false, detail: "Discord returned an invalid bot identity." };
247
+ const application = await this.#request("/applications/@me", { signal });
248
+ const applicationId = this.#string(application, "id");
249
+ if (!applicationId || applicationId !== this.applicationId) {
250
+ return { ok: false, detail: "Discord application identity does not match the configured application ID." };
251
+ }
252
+ return { ok: true, detail: "Discord bot and application credentials are valid.", botUserId };
253
+ } catch (error) {
254
+ if (signal?.aborted) return { ok: false, detail: "Discord configuration probe cancelled." };
255
+ const detail = error instanceof Error ? error.message : "Discord configuration probe failed.";
256
+ return { ok: false, detail };
257
+ }
258
+ }
259
+
260
+ async sendOneShotTest(input: {
261
+ channelId: string;
262
+ message: string;
263
+ signal?: AbortSignal;
264
+ }): Promise<DiscordOneShotTestResult> {
265
+ if (input.signal?.aborted) return { ok: false, detail: "Discord notification test cancelled." };
266
+ try {
267
+ const response = await this.#request(`/channels/${input.channelId}/messages`, {
268
+ method: "POST",
269
+ body: JSON.stringify({ content: input.message }),
270
+ signal: input.signal,
271
+ });
272
+ const messageId = this.#string(response, "id");
273
+ if (!messageId) {
274
+ return {
275
+ ok: false,
276
+ detail: "Discord may have accepted the message but returned no message receipt.",
277
+ uncertain: true,
278
+ };
279
+ }
280
+ return { ok: true, detail: "Discord notification test delivered.", messageId };
281
+ } catch (error) {
282
+ if (input.signal?.aborted) return { ok: false, detail: "Discord notification test cancelled." };
283
+ const detail = error instanceof Error ? error.message : "Discord notification test failed.";
284
+ return { ok: false, detail, uncertain: !detail.startsWith("Discord API request failed") };
285
+ }
286
+ }
287
+
233
288
  async start(onEvent: (event: DiscordInboundEvent) => Promise<void>): Promise<void> {
234
289
  if (!this.#stopped) return;
235
290
  this.#onEvent = onEvent;
@@ -31,6 +31,27 @@ export interface DiscordMessageComponent {
31
31
  }>;
32
32
  }
33
33
 
34
+ export interface DiscordConfigurationProbeResult {
35
+ ok: boolean;
36
+ detail: string;
37
+ botUserId?: string;
38
+ }
39
+
40
+ export interface DiscordOneShotTestResult {
41
+ ok: boolean;
42
+ detail: string;
43
+ messageId?: string;
44
+ uncertain?: boolean;
45
+ }
46
+
47
+ export interface DiscordDiagnosticProvider {
48
+ probeConfiguration(signal?: AbortSignal): Promise<DiscordConfigurationProbeResult>;
49
+ sendOneShotTest(input: {
50
+ channelId: string;
51
+ message: string;
52
+ signal?: AbortSignal;
53
+ }): Promise<DiscordOneShotTestResult>;
54
+ }
34
55
  export interface DiscordProvider {
35
56
  readonly applicationId: string;
36
57
  readonly botUserId: string;
@@ -67,6 +67,7 @@ import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } fro
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;
@@ -3285,18 +3295,34 @@ export function createNotificationsExtension(
3285
3295
  ? "blocked_identity"
3286
3296
  : "ready";
3287
3297
  }
3298
+ type ConfiguredDaemonOwnerResult = "ready" | "blocked_identity" | "blocked_identity_with_sibling";
3288
3299
  async function ensureConfiguredDaemonOwners(
3289
3300
  settings: Settings,
3290
3301
  cfg: NotificationConfig,
3291
3302
  cwd: string,
3292
3303
  id: string,
3293
3304
  onRegistered?: (registrationToken: string) => void,
3294
- ): Promise<boolean> {
3295
- if (isTelegramConfigured(cfg)) {
3296
- 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
+ }
3297
3323
  }
3298
3324
  await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
3299
- return true;
3325
+ return "ready";
3300
3326
  }
3301
3327
  const identityControlOperations = new Set([
3302
3328
  "session.new",
@@ -3484,7 +3510,7 @@ export function createNotificationsExtension(
3484
3510
  const id = sessionId(ctx);
3485
3511
  const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
3486
3512
  const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
3487
- const notificationsEnabledForSession = controller.query(ctx).effectiveEnabled;
3513
+ const notificationsEnabledForSession = controller.query(ctx).genericSessionEnabled;
3488
3514
  const sdkEnabledForSession =
3489
3515
  (options.sdkHostModeSupported ?? true) && shouldHostSdk(settings, isNotificationEligibleContext(ctx));
3490
3516
  const lifecycleRequired = lifecycleStartupCapability !== undefined;
@@ -3525,6 +3551,25 @@ export function createNotificationsExtension(
3525
3551
  }
3526
3552
 
3527
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;
3528
3573
  const lifecycleAgentDir = lifecycleRequired ? settings?.getAgentDir?.() : undefined;
3529
3574
  if (lifecycleRequired && !lifecycleAgentDir)
3530
3575
  return failLifecycleStartup("failed", "Lifecycle SDK startup requires an agent directory.");
@@ -3546,7 +3591,7 @@ export function createNotificationsExtension(
3546
3591
  nativeVersion: nativeBuildInfo().version,
3547
3592
  notificationServer: NotificationServer.prototype,
3548
3593
  });
3549
- server = new NotificationServer(id, token, stateRoot, true);
3594
+ server = new NotificationServer(id, token, endpointStateRoot, true);
3550
3595
  } catch (error) {
3551
3596
  if (lifecycleRequired) return failLifecycleStartup("failed", error);
3552
3597
  throw error;
@@ -5006,18 +5051,26 @@ export function createNotificationsExtension(
5006
5051
  if (notificationsEnabledForSession && settingsAvailable && settings) {
5007
5052
  try {
5008
5053
  let registrationToken: string | undefined;
5009
- if (
5010
- !(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
5011
- registrationToken = token;
5012
- }))
5013
- ) {
5054
+ const ownership = await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
5055
+ registrationToken = token;
5056
+ });
5057
+ if (ownership === "blocked_identity") {
5014
5058
  const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
5015
5059
  finishStartup(result);
5016
5060
  await cleanupAbandonedStartup();
5017
5061
  return result;
5018
5062
  }
5019
- 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) {
5020
5072
  runtime.notificationRootRegistration = { settings, cwd: ctx.cwd, registrationToken };
5073
+ }
5021
5074
  } catch (error) {
5022
5075
  const result = failLifecycleStartup("failed", error);
5023
5076
  finishStartup(result);
@@ -5059,8 +5112,8 @@ export function createNotificationsExtension(
5059
5112
  throwIfLifecycleStopped();
5060
5113
  const index = await new SessionIndex(agentDir).open();
5061
5114
  throwIfLifecycleStopped();
5062
- const locator = { repo: path.resolve(ctx.cwd), stateRoot };
5063
- 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;
5064
5117
  await host.registerWithBroker({
5065
5118
  // The endpoint is written before registration. Its exact mtime
5066
5119
  // binds this index generation to that discovery record.
@@ -5294,6 +5347,19 @@ export function createNotificationsExtension(
5294
5347
  return result.status === "started" || result.status === "already" ? "started" : result.status;
5295
5348
  },
5296
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
+ },
5297
5363
  refreshPolicy: (binding, policy) => {
5298
5364
  const runtime = runtimes.get(binding.sessionId);
5299
5365
  if (!runtime) return;
@@ -5339,8 +5405,8 @@ export function createNotificationsExtension(
5339
5405
  registrationToken = token;
5340
5406
  });
5341
5407
  const runtime = runtimes.get(binding.sessionId);
5342
- const configured = isTelegramConfigured(resolveSettings(options.settings).cfg);
5343
- 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) {
5344
5410
  runtime.notificationRootRegistration = { settings, cwd: binding.cwd, registrationToken };
5345
5411
  } else if (registrationToken !== undefined) {
5346
5412
  await unregisterNotificationRoot({
@@ -5364,11 +5430,14 @@ export function createNotificationsExtension(
5364
5430
  const id = sessionId(ctx);
5365
5431
  const command = args.trim().split(/\s+/, 1)[0]?.toLowerCase() || "status";
5366
5432
  const resolved = resolveSettings(options.settings);
5367
- const enabledWithoutLocalOff = isSessionNotificationsEnabled({
5433
+ const manualEligibilityEnv =
5434
+ process.env.GJC_NOTIFICATIONS === "0" ? { ...process.env, GJC_NOTIFICATIONS: undefined } : process.env;
5435
+ const enabledWithoutLocalOff = resolveGenericNotificationSessionEligibility({
5368
5436
  cfg: resolved.cfg,
5369
- env: process.env,
5437
+ env: manualEligibilityEnv,
5370
5438
  sessionDisabled: false,
5371
- });
5439
+ spawnedByGjc: options.spawnedByGjc,
5440
+ }).enabled;
5372
5441
 
5373
5442
  if (command === "off") {
5374
5443
  const result = await controller.setLocalEnabled(ctx, false);
@@ -5386,13 +5455,6 @@ export function createNotificationsExtension(
5386
5455
  ctx.ui.notify("Notifications are disabled for subagent sessions.", "warning");
5387
5456
  return;
5388
5457
  }
5389
- if (process.env.GJC_NOTIFICATIONS === "0") {
5390
- ctx.ui.notify(
5391
- "Notifications remain disabled: GJC_NOTIFICATIONS=0 is an authoritative opt-out.",
5392
- "warning",
5393
- );
5394
- return;
5395
- }
5396
5458
  if (!enabledWithoutLocalOff) {
5397
5459
  ctx.ui.notify(
5398
5460
  "Notifications are not configured. Run `gjc notify setup` or set GJC_NOTIFICATIONS=1.",
@@ -5401,7 +5463,7 @@ export function createNotificationsExtension(
5401
5463
  return;
5402
5464
  }
5403
5465
  const result = await controller.setLocalEnabled(ctx, true);
5404
- const enabled = result.status.running && result.status.effectiveEnabled;
5466
+ const enabled = result.status.running && result.status.genericSessionEnabled;
5405
5467
  const rotated = sessionId(ctx) !== id;
5406
5468
  if (rotated) await stopSession(id);
5407
5469
  const failed = result.outcome === "failed" || (!enabled && !rotated && activeRuntimeId !== id);
@@ -5426,7 +5488,7 @@ export function createNotificationsExtension(
5426
5488
  const status = controller.query(ctx);
5427
5489
  const runtime = runtimes.get(id);
5428
5490
  ctx.ui.notify(
5429
- `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"}.`,
5430
5492
  "info",
5431
5493
  );
5432
5494
  },