@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
@@ -1,9 +1,10 @@
1
1
  import {
2
+ type GenericNotificationSessionSource,
2
3
  getCurrentTelegramActivationMarker,
3
- isNotificationStreamingEnabled,
4
- isSessionNotificationsEnabled,
5
- isTelegramConfigured,
4
+ isProviderEffectivelyEnabled,
6
5
  type NotificationConfig,
6
+ resolveGenericNotificationSessionEligibility,
7
+ resolveGenericNotificationStreamPolicy,
7
8
  } from "./config";
8
9
 
9
10
  /** Minimal session-manager surface shared by extension, TUI, and headless hosts. */
@@ -39,6 +40,8 @@ export interface NotificationSessionRuntime<Context extends NotificationSessionC
39
40
  * emit a frame. `blocked_identity` is fail-closed and starts nothing.
40
41
  */
41
42
  ensureTelegramDaemon?(binding: BoundNotificationSession<Context>): Promise<TelegramDaemonPreflightResult>;
43
+ /** Rotates a running/default endpoint into Telegram-isolated chat scope. */
44
+ isolateTelegram?(binding: BoundNotificationSession<Context>): Promise<NotificationEndpointStartResult>;
42
45
  /** Refresh mutable delivery policy from the same configuration snapshot used for reconciliation. */
43
46
  refreshPolicy?(binding: BoundNotificationSession<Context>, policy: NotificationRuntimePolicy): void;
44
47
  /** Enables delivery only after the controller has committed a stable policy. */
@@ -48,9 +51,9 @@ export interface NotificationSessionRuntime<Context extends NotificationSessionC
48
51
  export interface NotificationSessionStatus {
49
52
  eligible: boolean;
50
53
  locallyEnabled: boolean;
51
- effectiveEnabled: boolean;
54
+ genericSessionEnabled: boolean;
55
+ genericEligibilitySource: GenericNotificationSessionSource;
52
56
  running: boolean;
53
- environment: "off" | "explicit" | "token" | "default";
54
57
  }
55
58
 
56
59
  export interface NotificationSessionReconcileResult {
@@ -61,6 +64,7 @@ export interface NotificationSessionReconcileResult {
61
64
  export interface NotificationRuntimePolicy {
62
65
  redact: boolean;
63
66
  verbosity: NotificationConfig["verbosity"];
67
+ /** Generic live-frame delivery policy; never proof of provider effectiveness. */
64
68
  stream: boolean;
65
69
  mode: "provisional" | "committed";
66
70
  }
@@ -72,14 +76,16 @@ export interface NotificationSessionControllerOptions {
72
76
  getConfig(): NotificationConfig;
73
77
  /** Kept as a reference so test and embedding hosts can supply their own environment. */
74
78
  env?: NodeJS.ProcessEnv;
79
+ /** This process was launched by a marked GJC child spawn site. */
80
+ spawnedByGjc?: boolean;
75
81
  }
76
82
 
77
83
  /**
78
84
  * Shared owner of notification session policy.
79
85
  *
80
- * Gate A is captured at creation. Gate B is evaluated for each session
81
- * operation with `isSessionNotificationsEnabled`. Gate C verifies complete
82
- * Telegram ownership before a generic endpoint is allowed to start.
86
+ * Gate A is captured at creation. Gate B is evaluated through the typed generic
87
+ * session policy. Provider effectiveness remains separate; Telegram owner proof
88
+ * runs only when Telegram is effective and has no inactive marker.
83
89
  */
84
90
  const MAX_RECONCILE_ATTEMPTS = 3;
85
91
 
@@ -87,7 +93,10 @@ export class NotificationSessionController {
87
93
  readonly #eligible: boolean;
88
94
  readonly #getConfig: () => NotificationConfig;
89
95
  readonly #env: NodeJS.ProcessEnv;
96
+ readonly #spawnedByGjc: boolean;
90
97
  readonly #disabledSessions = new Set<string>();
98
+ /** Explicit per-session opt-in overrides only the generic GJC_NOTIFICATIONS=0 auto-admission suppression. */
99
+ readonly #manualOptInSessions = new Set<string>();
91
100
  /** Sessions held inactive after a post-commit foreign daemon identity race. */
92
101
  readonly #blockedRuntimeSessions = new Set<string>();
93
102
  /** Sessions closed during host shutdown; no queued operation may restart them. */
@@ -100,6 +109,7 @@ export class NotificationSessionController {
100
109
  this.#eligible = options.eligible;
101
110
  this.#getConfig = options.getConfig;
102
111
  this.#env = options.env ?? process.env;
112
+ this.#spawnedByGjc = options.spawnedByGjc ?? false;
103
113
  }
104
114
 
105
115
  /** Attach the concrete generic endpoint implementation used by this host. */
@@ -140,6 +150,7 @@ export class NotificationSessionController {
140
150
  rekeySession(previousSessionId: string, nextSessionId: string): void {
141
151
  if (previousSessionId === nextSessionId) return;
142
152
  if (this.#disabledSessions.delete(previousSessionId)) this.#disabledSessions.add(nextSessionId);
153
+ if (this.#manualOptInSessions.delete(previousSessionId)) this.#manualOptInSessions.add(nextSessionId);
143
154
  if (this.#blockedRuntimeSessions.delete(previousSessionId)) this.#blockedRuntimeSessions.add(nextSessionId);
144
155
  if (this.#shuttingDownSessions.delete(previousSessionId)) this.#shuttingDownSessions.add(nextSessionId);
145
156
 
@@ -186,6 +197,38 @@ export class NotificationSessionController {
186
197
  return await this.#enqueue(binding, async () => {
187
198
  const runtime = this.#runtime as NotificationSessionRuntime<Context> | undefined;
188
199
  this.#refreshPolicy(runtime, binding, { redact: true, verbosity: "lean", stream: false }, "provisional");
200
+ let preserveSafeSibling = false;
201
+ try {
202
+ const cfg = this.#getConfig();
203
+ preserveSafeSibling =
204
+ isProviderEffectivelyEnabled(cfg, "discord") || isProviderEffectivelyEnabled(cfg, "slack");
205
+ } catch {
206
+ preserveSafeSibling = false;
207
+ }
208
+ if (preserveSafeSibling && runtime?.isolateTelegram) {
209
+ this.#blockedRuntimeSessions.add(binding.sessionId);
210
+ const outcome = await runtime.isolateTelegram(binding);
211
+ if (outcome === "started" || outcome === "already") {
212
+ this.#blockedRuntimeSessions.delete(binding.sessionId);
213
+ const cfg = this.#getConfig();
214
+ this.#refreshPolicy(
215
+ runtime,
216
+ binding,
217
+ {
218
+ redact: cfg.redact,
219
+ verbosity: cfg.verbosity,
220
+ stream: resolveGenericNotificationStreamPolicy({
221
+ cfg,
222
+ env: this.#env,
223
+ genericSessionEnabled: true,
224
+ }).enabled,
225
+ },
226
+ "committed",
227
+ );
228
+ runtime.activate?.(binding);
229
+ return true;
230
+ }
231
+ }
189
232
  if (runtime?.isRunning(binding)) {
190
233
  const stopped = await runtime.stop(binding);
191
234
  if (!stopped || runtime.isRunning(binding)) {
@@ -222,8 +265,10 @@ export class NotificationSessionController {
222
265
  if (enabled) {
223
266
  this.#disabledSessions.delete(binding.sessionId);
224
267
  this.#shuttingDownSessions.delete(binding.sessionId);
268
+ this.#manualOptInSessions.add(binding.sessionId);
225
269
  } else {
226
270
  this.#disabledSessions.add(binding.sessionId);
271
+ this.#manualOptInSessions.delete(binding.sessionId);
227
272
  }
228
273
  return await this.#reconcile(binding);
229
274
  });
@@ -256,49 +301,65 @@ export class NotificationSessionController {
256
301
  if (runtime?.isRunning(binding)) await runtime.stop(binding);
257
302
  return { outcome: "failed", status: this.#failClosedStatus(binding, runtime) };
258
303
  }
304
+ const nonTelegramEffective =
305
+ isProviderEffectivelyEnabled(cfg, "discord") || isProviderEffectivelyEnabled(cfg, "slack");
306
+ if (nonTelegramEffective) this.#blockedRuntimeSessions.delete(binding.sessionId);
259
307
  const status = this.#status(binding, cfg, runtime);
260
- if (!status.effectiveEnabled) {
308
+ if (!status.genericSessionEnabled) {
261
309
  if (runtime && status.running) await runtime.stop(binding);
262
310
  if (!this.#isCurrentConfig(cfg)) continue;
263
311
  return { outcome: status.running ? "stopped" : "disabled", status: this.#status(binding, cfg, runtime) };
264
312
  }
265
313
 
266
314
  if (!runtime) return { outcome: "disabled", status };
267
- if (getCurrentTelegramActivationMarker(cfg)) {
315
+ const telegramMarker = getCurrentTelegramActivationMarker(cfg);
316
+ const telegramEffective = isProviderEffectivelyEnabled(cfg, "telegram");
317
+ const nonTelegramEffectiveForTelegram =
318
+ isProviderEffectivelyEnabled(cfg, "discord") || isProviderEffectivelyEnabled(cfg, "slack");
319
+ if (telegramEffective && telegramMarker && !nonTelegramEffectiveForTelegram) {
268
320
  if (runtime.isRunning(binding)) await runtime.stop(binding);
269
- this.#blockedRuntimeSessions.add(binding.sessionId);
270
321
  if (!this.#isCurrentConfig(cfg)) continue;
271
322
  return { outcome: "disabled", status: this.#status(binding, cfg, runtime) };
272
323
  }
273
- if (isTelegramConfigured(cfg)) {
324
+ if (telegramEffective && !telegramMarker) {
325
+ let ensured: TelegramDaemonPreflightResult | undefined;
274
326
  try {
275
- const ensured = await runtime.ensureTelegramDaemon?.(binding);
276
- if (!this.#isCurrentConfig(cfg)) continue;
277
- if (ensured !== "ready") {
278
- if (runtime.isRunning(binding)) await runtime.stop(binding);
279
- this.#blockedRuntimeSessions.add(binding.sessionId);
280
- return {
281
- outcome: ensured === "failed" ? "failed" : "disabled",
282
- status: this.#status(binding, cfg, runtime),
283
- };
284
- }
327
+ ensured = await runtime.ensureTelegramDaemon?.(binding);
285
328
  } catch {
286
- if (!this.#isCurrentConfig(cfg)) continue;
329
+ ensured = "failed";
330
+ }
331
+ if (!this.#isCurrentConfig(cfg)) continue;
332
+ if (ensured !== "ready" && !nonTelegramEffectiveForTelegram) {
287
333
  if (runtime.isRunning(binding)) await runtime.stop(binding);
288
334
  this.#blockedRuntimeSessions.add(binding.sessionId);
289
- return { outcome: "failed", status: this.#status(binding, cfg, runtime) };
335
+ return {
336
+ outcome: ensured === "failed" ? "failed" : "disabled",
337
+ status: this.#status(binding, cfg, runtime),
338
+ };
339
+ }
340
+ if (ensured !== "ready" && nonTelegramEffectiveForTelegram) {
341
+ const isolated = await runtime.isolateTelegram?.(binding);
342
+ if (isolated !== "started" && isolated !== "already") {
343
+ if (runtime.isRunning(binding)) await runtime.stop(binding);
344
+ this.#blockedRuntimeSessions.add(binding.sessionId);
345
+ return { outcome: "failed", status: this.#status(binding, cfg, runtime) };
346
+ }
290
347
  }
291
348
  }
292
349
 
293
350
  const current = this.#status(binding, cfg, runtime);
294
- if (!current.effectiveEnabled || !this.#isCurrentConfig(cfg)) continue;
351
+ if (!current.genericSessionEnabled || !this.#isCurrentConfig(cfg)) continue;
295
352
  this.#refreshPolicy(
296
353
  runtime,
297
354
  binding,
298
355
  {
299
356
  redact: cfg.redact,
300
357
  verbosity: cfg.verbosity,
301
- stream: isNotificationStreamingEnabled({ cfg, env: this.#env }),
358
+ stream: resolveGenericNotificationStreamPolicy({
359
+ cfg,
360
+ env: this.#env,
361
+ genericSessionEnabled: current.genericSessionEnabled,
362
+ }).enabled,
302
363
  },
303
364
  "committed",
304
365
  );
@@ -311,7 +372,11 @@ export class NotificationSessionController {
311
372
  {
312
373
  redact: cfg.redact,
313
374
  verbosity: cfg.verbosity,
314
- stream: isNotificationStreamingEnabled({ cfg, env: this.#env }),
375
+ stream: resolveGenericNotificationStreamPolicy({
376
+ cfg,
377
+ env: this.#env,
378
+ genericSessionEnabled: current.genericSessionEnabled,
379
+ }).enabled,
315
380
  },
316
381
  "committed",
317
382
  );
@@ -325,7 +390,7 @@ export class NotificationSessionController {
325
390
  }
326
391
  runtime.activate?.(binding);
327
392
  const afterStart = this.#status(binding, cfg, runtime);
328
- if (!afterStart.effectiveEnabled) {
393
+ if (!afterStart.genericSessionEnabled) {
329
394
  if (afterStart.running) await runtime.stop(binding);
330
395
  return {
331
396
  outcome: afterStart.running ? "stopped" : "disabled",
@@ -357,9 +422,9 @@ export class NotificationSessionController {
357
422
  return {
358
423
  eligible: this.#eligible,
359
424
  locallyEnabled: false,
360
- effectiveEnabled: false,
425
+ genericSessionEnabled: false,
426
+ genericEligibilitySource: "none",
361
427
  running: runtime?.isRunning(binding) ?? false,
362
- environment: "off",
363
428
  };
364
429
  }
365
430
 
@@ -414,26 +479,36 @@ export class NotificationSessionController {
414
479
  const locallyEnabled = !this.#disabledSessions.has(binding.sessionId);
415
480
  const blockedRuntime = this.#blockedRuntimeSessions.has(binding.sessionId);
416
481
  const shuttingDown = this.#shuttingDownSessions.has(binding.sessionId);
417
- const effectiveEnabled =
418
- !blockedRuntime &&
482
+ const manualOptIn = this.#manualOptInSessions.has(binding.sessionId);
483
+ const eligibilityEnv =
484
+ manualOptIn && this.#env.GJC_NOTIFICATIONS === "0"
485
+ ? { ...this.#env, GJC_NOTIFICATIONS: undefined }
486
+ : this.#env;
487
+ const eligibility = resolveGenericNotificationSessionEligibility({
488
+ cfg,
489
+ env: eligibilityEnv,
490
+ sessionDisabled: !locallyEnabled,
491
+ spawnedByGjc: this.#spawnedByGjc,
492
+ });
493
+ const telegramMarkerBlocksOnlyProvider =
494
+ Boolean(getCurrentTelegramActivationMarker(cfg)) &&
495
+ isProviderEffectivelyEnabled(cfg, "telegram") &&
496
+ !isProviderEffectivelyEnabled(cfg, "discord") &&
497
+ !isProviderEffectivelyEnabled(cfg, "slack");
498
+ const telegramBlockedWithoutSibling =
499
+ blockedRuntime && !isProviderEffectivelyEnabled(cfg, "discord") && !isProviderEffectivelyEnabled(cfg, "slack");
500
+ const genericSessionEnabled =
501
+ !telegramBlockedWithoutSibling &&
419
502
  !shuttingDown &&
420
- !getCurrentTelegramActivationMarker(cfg) &&
503
+ !telegramMarkerBlocksOnlyProvider &&
421
504
  this.#eligible &&
422
- isSessionNotificationsEnabled({ cfg, env: this.#env, sessionDisabled: !locallyEnabled });
423
- const environment =
424
- this.#env.GJC_NOTIFICATIONS === "0"
425
- ? "off"
426
- : this.#env.GJC_NOTIFICATIONS === "1"
427
- ? "explicit"
428
- : this.#env.GJC_NOTIFICATIONS_TOKEN
429
- ? "token"
430
- : "default";
505
+ eligibility.enabled;
431
506
  return {
432
507
  eligible: this.#eligible,
433
508
  locallyEnabled,
434
- effectiveEnabled,
509
+ genericSessionEnabled,
510
+ genericEligibilitySource: eligibility.source,
435
511
  running: runtime?.isRunning(binding) ?? false,
436
- environment,
437
512
  };
438
513
  }
439
514
  }
@@ -1,5 +1,8 @@
1
1
  import type {
2
+ SlackConfigurationProbeResult,
3
+ SlackDiagnosticProvider,
2
4
  SlackMessageSearchResult,
5
+ SlackOneShotTestResult,
3
6
  SlackPostedMessage,
4
7
  SlackProviderClient,
5
8
  SlackSocketEnvelope,
@@ -69,6 +72,8 @@ type SlackApiResponse = {
69
72
  message?: unknown;
70
73
  messages?: unknown;
71
74
  retry_after?: unknown;
75
+ team_id?: unknown;
76
+ user_id?: unknown;
72
77
  };
73
78
 
74
79
  function string(value: unknown): string | undefined {
@@ -96,7 +101,7 @@ function socketFromGlobal(url: string): SlackWebSocket {
96
101
  * Production Socket Mode and Web API client. It keeps all connection state in
97
102
  * memory and deliberately never maintains a Slack cursor.
98
103
  */
99
- export class SlackLiveProvider implements SlackProviderClient {
104
+ export class SlackLiveProvider implements SlackProviderClient, SlackDiagnosticProvider {
100
105
  readonly #appToken: string;
101
106
  readonly #botToken: string;
102
107
  readonly #fetch: (input: string, init?: RequestInit) => Promise<Response>;
@@ -140,6 +145,68 @@ export class SlackLiveProvider implements SlackProviderClient {
140
145
  this.#activityTimeoutMs = Math.max(1, options.activityTimeoutMs ?? 60_000);
141
146
  }
142
147
 
148
+ async probeConfiguration(signal?: AbortSignal): Promise<SlackConfigurationProbeResult> {
149
+ if (signal?.aborted) return { ok: false, detail: "Slack configuration probe cancelled." };
150
+ try {
151
+ const bot = await this.#request("auth.test", this.#botToken, {}, false, signal);
152
+ if (bot.ok !== true) return { ok: false, detail: "Slack bot token authorization failed." };
153
+ const app = await this.#request("apps.connections.open", this.#appToken, {}, false, signal);
154
+ if (app.ok !== true || !string(app.url)) {
155
+ return { ok: false, detail: "Slack app token Socket Mode authorization failed." };
156
+ }
157
+ return {
158
+ ok: true,
159
+ detail: "Slack bot and app credentials are valid.",
160
+ teamId: string(bot.team_id),
161
+ userId: string(bot.user_id),
162
+ };
163
+ } catch (error) {
164
+ if (signal?.aborted) return { ok: false, detail: "Slack configuration probe cancelled." };
165
+ return { ok: false, detail: error instanceof Error ? error.message : "Slack configuration probe failed." };
166
+ }
167
+ }
168
+
169
+ async sendOneShotTest(input: {
170
+ channel: string;
171
+ message: string;
172
+ idempotencyKey: string;
173
+ signal?: AbortSignal;
174
+ }): Promise<SlackOneShotTestResult> {
175
+ if (input.signal?.aborted) return { ok: false, detail: "Slack notification test cancelled." };
176
+ try {
177
+ const response = await this.#request(
178
+ "chat.postMessage",
179
+ this.#botToken,
180
+ { channel: input.channel, text: input.message, client_msg_id: input.idempotencyKey },
181
+ true,
182
+ input.signal,
183
+ );
184
+ if (response.ok !== true) throw new SlackProviderError("web_api", "chat.postMessage");
185
+ const posted = this.#message(response);
186
+ if (!posted) {
187
+ return {
188
+ ok: false,
189
+ detail: "Slack may have accepted the message but returned no message receipt.",
190
+ uncertain: true,
191
+ };
192
+ }
193
+ return {
194
+ ok: true,
195
+ detail: "Slack notification test delivered.",
196
+ channel: posted.channel,
197
+ timestamp: posted.ts,
198
+ };
199
+ } catch (error) {
200
+ if (input.signal?.aborted) return { ok: false, detail: "Slack notification test cancelled." };
201
+ const uncertain = error instanceof SlackProviderError && error.mayHaveBeenAccepted;
202
+ return {
203
+ ok: false,
204
+ detail: error instanceof Error ? error.message : "Slack notification test failed.",
205
+ ...(uncertain ? { uncertain: true } : {}),
206
+ };
207
+ }
208
+ }
209
+
143
210
  async start(onEnvelope: (envelope: SlackSocketEnvelope) => void | Promise<void>): Promise<void> {
144
211
  this.#lifecycleGeneration++;
145
212
  this.#onEnvelope = onEnvelope;
@@ -310,17 +377,26 @@ export class SlackLiveProvider implements SlackProviderClient {
310
377
  token: string,
311
378
  body: Record<string, string | undefined>,
312
379
  retryRateLimit = false,
380
+ signal?: AbortSignal,
313
381
  ): Promise<SlackApiResponse> {
314
382
  for (let attempt = 0; ; attempt++) {
383
+ signal?.throwIfAborted();
315
384
  let response: Response;
316
385
  try {
317
386
  response = await this.#fetch(`${SLACK_API}/${operation}`, {
318
387
  method: "POST",
319
388
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json; charset=utf-8" },
320
389
  body: JSON.stringify(body),
390
+ signal,
321
391
  });
322
392
  } catch {
323
- throw new SlackProviderError("connection", operation);
393
+ throw new SlackProviderError(
394
+ "connection",
395
+ operation,
396
+ undefined,
397
+ undefined,
398
+ operation === "chat.postMessage",
399
+ );
324
400
  }
325
401
  let parsed: unknown;
326
402
  try {
@@ -15,6 +15,30 @@ export interface SlackMessageSearchResult {
15
15
  client_msg_id?: string;
16
16
  }
17
17
 
18
+ export interface SlackConfigurationProbeResult {
19
+ ok: boolean;
20
+ detail: string;
21
+ teamId?: string;
22
+ userId?: string;
23
+ }
24
+
25
+ export interface SlackOneShotTestResult {
26
+ ok: boolean;
27
+ detail: string;
28
+ channel?: string;
29
+ timestamp?: string;
30
+ uncertain?: boolean;
31
+ }
32
+
33
+ export interface SlackDiagnosticProvider {
34
+ probeConfiguration(signal?: AbortSignal): Promise<SlackConfigurationProbeResult>;
35
+ sendOneShotTest(input: {
36
+ channel: string;
37
+ message: string;
38
+ idempotencyKey: string;
39
+ signal?: AbortSignal;
40
+ }): Promise<SlackOneShotTestResult>;
41
+ }
18
42
  /** Minimal Socket Mode + Web API seam. Implementations may wrap the official Slack SDK. */
19
43
  export interface SlackProviderClient {
20
44
  start(onEnvelope: (envelope: SlackSocketEnvelope) => void | Promise<void>): Promise<void>;
@@ -6,7 +6,8 @@ import { applyAtomicYamlPatches, setByPath } from "../../config/atomic-yaml-patc
6
6
  import type { Settings } from "../../config/settings";
7
7
  import {
8
8
  getNotificationConfig,
9
- isTelegramConfigured,
9
+ isProviderEffectivelyEnabled,
10
+ isTelegramComplete,
10
11
  type NotificationSettingsReader,
11
12
  parseNotificationSettingsSnapshot,
12
13
  } from "./config";
@@ -76,6 +77,8 @@ export function createLightweightDaemonSettings(input: {
76
77
  switch (pathName) {
77
78
  case "notifications.enabled":
78
79
  return snapshot.enabled;
80
+ case "notifications.telegram.enabled":
81
+ return snapshot.telegram.enabled;
79
82
  case "notifications.telegram.botToken":
80
83
  return snapshot.telegram.botToken;
81
84
  case "notifications.telegram.chatId":
@@ -84,6 +87,8 @@ export function createLightweightDaemonSettings(input: {
84
87
  return snapshot.telegram.btw.enabled;
85
88
  case "notifications.telegram.streaming.enabled":
86
89
  return snapshot.telegram.streaming.enabled;
90
+ case "notifications.discord.enabled":
91
+ return snapshot.discord.enabled;
87
92
  case "notifications.discord.botToken":
88
93
  return snapshot.discord.botToken;
89
94
  case "notifications.discord.applicationId":
@@ -92,6 +97,8 @@ export function createLightweightDaemonSettings(input: {
92
97
  return snapshot.discord.guildId;
93
98
  case "notifications.discord.parentChannelId":
94
99
  return snapshot.discord.parentChannelId;
100
+ case "notifications.slack.enabled":
101
+ return snapshot.slack.enabled;
95
102
  case "notifications.slack.botToken":
96
103
  return snapshot.slack.botToken;
97
104
  case "notifications.slack.appToken":
@@ -223,7 +230,7 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
223
230
  const resolvedAgentDir = agentDir ?? process.env.GJC_CODING_AGENT_DIR ?? path.join(process.cwd(), ".gjc", "agent");
224
231
  const settings = await resolveDaemonSettings(resolvedAgentDir, deps);
225
232
  const cfg = getNotificationConfig(settings);
226
- if (!isTelegramConfigured(cfg)) return;
233
+ if (!isProviderEffectivelyEnabled(cfg, "telegram") || !isTelegramComplete(cfg)) return;
227
234
  const Daemon: TelegramDaemonConstructor = deps.DaemonImpl ?? TelegramNotificationDaemon;
228
235
  const readState = deps.readDaemonState ?? readDaemonState;
229
236
  const daemon = new Daemon({
@@ -49,11 +49,16 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
49
49
  * user-created topic adoption (forum-topic folder picker). Generation 36 bound
50
50
  * managed-session replacement to exact native filesystem authority; generation
51
51
  * 37 retires that binding (revert of #3489, which stalled POSIX artifact
52
- * cleanup); generation 38 binds exact cleanup to parent and link-count authority;
53
- * generation 39 hardens exact Bash process-tree ownership, settlement, and
54
- * descendant cleanup authority.
52
+ * cleanup); generation 38 binds exact cleanup to parent and link-count authority.
53
+ * Generation 39 applies rustfmt and clippy-equivalent cleanup to the pi-shell
54
+ * process-tree authority (#3682); generation 40 hardens exact Bash process-tree
55
+ * ownership, settlement, and descendant cleanup authority. Generation 38 also
56
+ * adds durable provider-intent admission without changing owner, reclaim,
57
+ * signal, or spawn authority.
58
+ * Generation 41 applies first-class provider-settings admission to Telegram
59
+ * lifecycle controls.
55
60
  */
56
- export const DAEMON_GENERATION = 39;
61
+ export const DAEMON_GENERATION = 41;
57
62
 
58
63
  /**
59
64
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
@@ -26,7 +26,7 @@ import { OWNERSHIP_MISMATCH_MESSAGE, ownershipMismatchRecovery } from "../../dae
26
26
  import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
27
27
  import { isProcessIncarnation } from "../broker/process-incarnation";
28
28
 
29
- import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
29
+ import { getNotificationConfig, isTelegramComplete, tokenFingerprint } from "./config";
30
30
  import { exactUnlinkNotificationFile, readNotificationEndpointFile } from "./notification-service";
31
31
  import {
32
32
  type AttestedLegacyDaemonOwner,
@@ -221,7 +221,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
221
221
  async status(): Promise<DaemonStatus> {
222
222
  const runtime = this.runtimeInfo();
223
223
  const cfg = getNotificationConfig(this.settings);
224
- const configured = isTelegramConfigured(cfg);
224
+ const configured = isTelegramComplete(cfg);
225
225
  if (!configured) {
226
226
  return { kind: this.kind, configured: false, health: "not_configured", runtime };
227
227
  }
@@ -369,7 +369,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
369
369
  #currentConfigurationMatches(tokenFingerprintValue: string, chatId: string): boolean {
370
370
  const current = getNotificationConfig(this.settings);
371
371
  return (
372
- isTelegramConfigured(current) &&
372
+ isTelegramComplete(current) &&
373
373
  tokenFingerprint(current.botToken) === tokenFingerprintValue &&
374
374
  current.chatId === chatId
375
375
  );
@@ -11,7 +11,7 @@ import type { DaemonRuntimeInfo } from "../../daemon/control-types";
11
11
  import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
12
12
  import { resizeImageBuffer } from "../../utils/image-resize";
13
13
  import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
14
- import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
14
+ import { getNotificationConfig, isProviderEffectivelyEnabled, isTelegramComplete, tokenFingerprint } from "./config";
15
15
  import {
16
16
  parseInThreadConfigCommand,
17
17
  parseRichToggleCommand,
@@ -1565,18 +1565,26 @@ async function reapNotificationLeakArtifact(input: {
1565
1565
  now: number;
1566
1566
  graceMs: number;
1567
1567
  }): Promise<NotificationArtifactReapOutcome> {
1568
- if (path.basename(input.file).startsWith(".gjc-exact-unlink-placeholder-")) return "retained";
1569
- if (!input.fs.readEndpointFile || !input.fs.exactUnlink) return "unchanged";
1570
- const endpoint = await input.fs.readEndpointFile(input.file);
1571
- if (!(await isSingleLinkRegularFile(input.fs, input.file))) return "unchanged";
1572
- const age = input.now - Number(endpoint.identity.mtimeNs / 1_000_000n);
1573
- if (age < input.graceMs) return "unchanged";
1574
- return await reapNotificationArtifactExactly(
1575
- input.fs,
1576
- input.file,
1577
- endpoint.identity,
1578
- `.gjc-exact-unlink-placeholder-${crypto.randomUUID()}.json`,
1579
- );
1568
+ const name = path.basename(input.file);
1569
+ if (name.startsWith(".gjc-exact-unlink-placeholder-")) return "retained";
1570
+ if (name.startsWith(".gjc-delete-notification-staging-temp-")) {
1571
+ if (!input.fs.readEndpointFile || !input.fs.exactUnlink) return "unchanged";
1572
+ const endpoint = await input.fs.readEndpointFile(input.file);
1573
+ if (!(await isSingleLinkRegularFile(input.fs, input.file))) return "unchanged";
1574
+ const age = input.now - Number(endpoint.identity.mtimeNs / 1_000_000n);
1575
+ if (age < input.graceMs) return "unchanged";
1576
+ return await reapNotificationArtifactExactly(
1577
+ input.fs,
1578
+ input.file,
1579
+ endpoint.identity,
1580
+ `.gjc-exact-unlink-placeholder-${crypto.randomUUID()}.json`,
1581
+ );
1582
+ }
1583
+ if (!input.fs.stat) return "unchanged";
1584
+ const stat = await input.fs.stat(input.file);
1585
+ if (Number.isFinite(stat.mtimeMs) && input.now - stat.mtimeMs < input.graceMs) return "unchanged";
1586
+ await input.fs.unlink(input.file);
1587
+ return "removed";
1580
1588
  }
1581
1589
 
1582
1590
  /**
@@ -3404,7 +3412,7 @@ async function ensureTelegramDaemonRunningDetailedOnce(
3404
3412
  deps: TelegramDaemonDeps = {},
3405
3413
  ): Promise<EnsureTelegramDaemonDetailedResult> {
3406
3414
  const cfg = getNotificationConfig(input.settings);
3407
- if (!isTelegramConfigured(cfg)) return "disabled";
3415
+ if (!isProviderEffectivelyEnabled(cfg, "telegram") || !isTelegramComplete(cfg)) return "disabled";
3408
3416
  const root = notificationRootForCwd(input.cwd);
3409
3417
  const fp = tokenFingerprint(cfg.botToken);
3410
3418
  // A live v0.10 parent has no stable process authority on Windows. Never turn an
@@ -3664,7 +3672,7 @@ export async function ensureTelegramDaemonRunningDetailed(
3664
3672
  if (input.registerRoot === false || (result !== "attached" && result !== "spawned")) return result;
3665
3673
 
3666
3674
  const cfg = getNotificationConfig(input.settings);
3667
- if (!isTelegramConfigured(cfg)) return "disabled";
3675
+ if (!isProviderEffectivelyEnabled(cfg, "telegram") || !isTelegramComplete(cfg)) return "disabled";
3668
3676
  const ownerIsCurrent = async (): Promise<boolean> => {
3669
3677
  const snapshot = await readOwnerFreshnessSnapshot({ settings: input.settings, fs: deps.fs });
3670
3678
  return isCurrentCompatibleOwner({
@@ -33,8 +33,10 @@ export class SdkDiscoveryError extends Error {
33
33
  }
34
34
  }
35
35
 
36
- function endpointDirectory(repo: string): string {
37
- return path.join(repo, ".gjc", "state", "sdk");
36
+ export type SdkSessionEndpointScope = "default" | "chat";
37
+
38
+ function endpointDirectory(repo: string, scope: SdkSessionEndpointScope = "default"): string {
39
+ return scope === "chat" ? path.join(repo, ".gjc", "state", "chat", "sdk") : path.join(repo, ".gjc", "state", "sdk");
38
40
  }
39
41
 
40
42
  function parseEndpoint(sessionId: string, file: string, value: unknown): SdkSessionEndpoint {
@@ -101,9 +103,13 @@ export async function listSdkSessionEndpoints(repo: string): Promise<SdkSessionE
101
103
  }
102
104
 
103
105
  /** Resolves one per-session SDK endpoint discovery file. */
104
- export async function readSdkSessionEndpoint(repo: string, sessionId: string): Promise<SdkSessionEndpoint | null> {
106
+ export async function readSdkSessionEndpoint(
107
+ repo: string,
108
+ sessionId: string,
109
+ scope: SdkSessionEndpointScope = "default",
110
+ ): Promise<SdkSessionEndpoint | null> {
105
111
  if (!sessionId || sessionId.includes(path.sep) || sessionId.includes("/")) return null;
106
- const file = path.join(endpointDirectory(repo), `${sessionId}.json`);
112
+ const file = path.join(endpointDirectory(repo, scope), `${sessionId}.json`);
107
113
  try {
108
114
  return parseEndpoint(sessionId, file, JSON.parse(await fs.readFile(file, "utf8")));
109
115
  } catch (error) {