@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.
- package/CHANGELOG.md +19 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +44 -24
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +382 -45
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/telegram-send.ts +4 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type GenericNotificationSessionSource,
|
|
2
3
|
getCurrentTelegramActivationMarker,
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
324
|
+
if (telegramEffective && !telegramMarker) {
|
|
325
|
+
let ensured: TelegramDaemonPreflightResult | undefined;
|
|
274
326
|
try {
|
|
275
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|
418
|
-
|
|
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
|
-
!
|
|
503
|
+
!telegramMarkerBlocksOnlyProvider &&
|
|
421
504
|
this.#eligible &&
|
|
422
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
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({
|
|
@@ -50,8 +50,15 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
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
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.
|
|
53
60
|
*/
|
|
54
|
-
export const DAEMON_GENERATION =
|
|
61
|
+
export const DAEMON_GENERATION = 41;
|
|
55
62
|
|
|
56
63
|
/**
|
|
57
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,
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1569
|
-
if (
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
input.
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
37
|
-
|
|
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(
|
|
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) {
|