@gajae-code/coding-agent 0.11.4 → 0.11.5
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 +23 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +48 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/commands/sdk.ts +7 -2
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-runtime.ts +38 -12
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -24
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +321 -0
- package/src/modes/components/transcript-viewer-overlay.ts +39 -17
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +70 -8
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/session-index.ts +122 -64
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1788 -267
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +200 -38
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +129 -32
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/index.ts +4 -0
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
package/src/sdk/bus/index.ts
CHANGED
|
@@ -33,12 +33,15 @@ import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
|
|
|
33
33
|
import { logger, postmortem, prompt, VERSION } from "@gajae-code/utils";
|
|
34
34
|
import { Settings } from "../../config/settings";
|
|
35
35
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
36
|
-
import
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
37
|
+
import {
|
|
38
|
+
NotificationGatePolicyChangedError,
|
|
39
|
+
type WorkflowGateEmitter,
|
|
40
|
+
type WorkflowGateTerminalController,
|
|
41
|
+
type WorkflowGateTerminalProof,
|
|
40
42
|
} from "../../modes/shared/agent-wire/workflow-gate-broker";
|
|
41
43
|
import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
|
|
44
|
+
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
42
45
|
import { parseThinkingLevel } from "../../thinking";
|
|
43
46
|
import type {
|
|
44
47
|
AskAnswerRequest,
|
|
@@ -81,6 +84,7 @@ import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMess
|
|
|
81
84
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
82
85
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
83
86
|
import {
|
|
87
|
+
ASK_SELECTED_ACK_CAPABILITY,
|
|
84
88
|
type EnsureDaemonResult,
|
|
85
89
|
endpointAuthorityDigest,
|
|
86
90
|
ensureTelegramDaemonRunningDetailed,
|
|
@@ -880,6 +884,12 @@ interface SessionRuntime {
|
|
|
880
884
|
workflowGate?: WorkflowGateEmitter;
|
|
881
885
|
gatePresentations?: PresentationArbiter;
|
|
882
886
|
redact: boolean;
|
|
887
|
+
/** Last stable policy's redaction state, retained while provisional policy is held. */
|
|
888
|
+
committedRedact: boolean;
|
|
889
|
+
/** Provisional policy suppresses delivery without changing committed-side effects. */
|
|
890
|
+
policySuspended: boolean;
|
|
891
|
+
/** Monotonic policy epoch fences asynchronous notification delivery. */
|
|
892
|
+
policyGeneration: number;
|
|
883
893
|
/** True only after the exact host generation was registered with the broker index. */
|
|
884
894
|
brokerRegistrationActive: boolean;
|
|
885
895
|
/** Terminal cleanup proof retained across retries; each owner is released at most once after proof. */
|
|
@@ -910,6 +920,8 @@ interface SessionRuntime {
|
|
|
910
920
|
error: { code: string; message: string };
|
|
911
921
|
},
|
|
912
922
|
) => void;
|
|
923
|
+
/** Publishes one canonical agent-wire event to the client that owns the active prompt. */
|
|
924
|
+
emitPromptEvent: (event: AgentSessionEvent) => void;
|
|
913
925
|
/** Inbound Telegram update ids injected but not yet consumed by a turn. */
|
|
914
926
|
pendingInbound: Set<number>;
|
|
915
927
|
/** Latest assistant text of the in-flight turn (from message_update). */
|
|
@@ -926,6 +938,12 @@ interface SessionRuntime {
|
|
|
926
938
|
/** True between turn_end and the next turn_start: drops late async message_update
|
|
927
939
|
* frames so a stale live edit can never be emitted after the finalized turn. */
|
|
928
940
|
turnClosed?: boolean;
|
|
941
|
+
/** Finalized while provisional policy was held; flush exactly once on stable activation. */
|
|
942
|
+
pendingFinal?: { text?: string; messageRef?: string };
|
|
943
|
+
/** Durable gates emitted while ownership is provisional; presented only after stable activation. */
|
|
944
|
+
deferredGatePresentations: Array<() => void>;
|
|
945
|
+
/** SDK control frames received during provisional ownership; replayed only after stable activation. */
|
|
946
|
+
deferredInboundControls: Array<() => void>;
|
|
929
947
|
/** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
|
|
930
948
|
inFlightTools: Map<string, { toolName: string; args: unknown }>;
|
|
931
949
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
@@ -981,6 +999,7 @@ type SessionStartResult = {
|
|
|
981
999
|
status: SessionStartStatus;
|
|
982
1000
|
runtime?: SessionRuntime;
|
|
983
1001
|
failure?: SdkStartupFailure;
|
|
1002
|
+
suppressExtensionError?: boolean;
|
|
984
1003
|
};
|
|
985
1004
|
|
|
986
1005
|
function pushSessionFrame(
|
|
@@ -1049,6 +1068,8 @@ const defaultConfig: NotificationConfig = {
|
|
|
1049
1068
|
idleTimeoutMs: 60_000,
|
|
1050
1069
|
rich: { enabled: true },
|
|
1051
1070
|
richDraft: { enabled: false },
|
|
1071
|
+
toolActivity: { enabled: true },
|
|
1072
|
+
streaming: { enabled: true },
|
|
1052
1073
|
topics: {},
|
|
1053
1074
|
btw: { enabled: true },
|
|
1054
1075
|
};
|
|
@@ -1057,13 +1078,6 @@ export function notificationsEnabled(): boolean {
|
|
|
1057
1078
|
return process.env.GJC_NOTIFICATIONS === "1" || Boolean(process.env.GJC_NOTIFICATIONS_TOKEN);
|
|
1058
1079
|
}
|
|
1059
1080
|
|
|
1060
|
-
// Live streaming (opt-in): emit throttled non-finalized `turn_stream` frames as
|
|
1061
|
-
// the assistant message streams so remote clients can edit ONE message live. The
|
|
1062
|
-
// finalized frame (turn_end) carries the same messageRef and stays authoritative,
|
|
1063
|
-
// so a dropped live frame self-heals. Off unless GJC_NOTIFICATIONS_STREAM=1.
|
|
1064
|
-
function streamingEnabled(): boolean {
|
|
1065
|
-
return process.env.GJC_NOTIFICATIONS_STREAM === "1";
|
|
1066
|
-
}
|
|
1067
1081
|
function streamIntervalMs(): number {
|
|
1068
1082
|
return Math.max(200, Number(process.env.GJC_NOTIFICATIONS_STREAM_INTERVAL_MS) || 500);
|
|
1069
1083
|
}
|
|
@@ -2728,6 +2742,22 @@ function sdkInboundFrame(commandJson: string | undefined): Record<string, unknow
|
|
|
2728
2742
|
return undefined;
|
|
2729
2743
|
}
|
|
2730
2744
|
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Ensures every configured chat-provider daemon is ready before the SDK
|
|
2747
|
+
* publishes session identity. A rejected ensure is startup-fatal: presenting
|
|
2748
|
+
* an identity for a transport that never became available is false success.
|
|
2749
|
+
*/
|
|
2750
|
+
export async function ensureConfiguredProviderDaemons(
|
|
2751
|
+
settings: Settings,
|
|
2752
|
+
cfg: NotificationConfig,
|
|
2753
|
+
ensureProviderDaemon: (provider: "discord" | "slack", settings: Settings) => Promise<unknown> = (
|
|
2754
|
+
provider,
|
|
2755
|
+
configuredSettings,
|
|
2756
|
+
) => (provider === "discord" ? ensureDiscordDaemon(configuredSettings) : ensureSlackDaemon(configuredSettings)),
|
|
2757
|
+
): Promise<void> {
|
|
2758
|
+
if (isDiscordConfigured(cfg)) await ensureProviderDaemon("discord", settings);
|
|
2759
|
+
if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
|
|
2760
|
+
}
|
|
2731
2761
|
|
|
2732
2762
|
export function createNotificationsExtension(
|
|
2733
2763
|
api: ExtensionAPI,
|
|
@@ -2738,6 +2768,7 @@ export function createNotificationsExtension(
|
|
|
2738
2768
|
cwd: string;
|
|
2739
2769
|
sessionId: string;
|
|
2740
2770
|
}) => Promise<EnsureDaemonResult>;
|
|
2771
|
+
ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<unknown>;
|
|
2741
2772
|
/** Suppress auto-delivery for a GJC-spawned child under `sessionScope=primary`. */
|
|
2742
2773
|
spawnedByGjc?: boolean;
|
|
2743
2774
|
controller?: NotificationSessionController;
|
|
@@ -2746,6 +2777,10 @@ export function createNotificationsExtension(
|
|
|
2746
2777
|
|
|
2747
2778
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2748
2779
|
runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2780
|
+
/** Observes settlement of optional session-branch startup after reconciliation completes. */
|
|
2781
|
+
onBranchStartupSettled?: (receipt: { sessionId: string; status: SessionStartResult["status"] }) => void;
|
|
2782
|
+
readNotificationFile?: (path: string) => Promise<Buffer>;
|
|
2783
|
+
readNotificationDiffStat?: (cwd: string) => Promise<string | undefined>;
|
|
2749
2784
|
} = {},
|
|
2750
2785
|
): void {
|
|
2751
2786
|
const lifecycleStartupCapability = lifecycleStartupCapabilityForApi(api);
|
|
@@ -2761,9 +2796,12 @@ export function createNotificationsExtension(
|
|
|
2761
2796
|
// exact runtime object retains authority for an explicit idempotent retry.
|
|
2762
2797
|
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2763
2798
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2799
|
+
const branchStartupTasks = new Set<Promise<void>>();
|
|
2764
2800
|
let activeRuntimeId: string | undefined;
|
|
2765
2801
|
let identityControlInFlight = false;
|
|
2766
|
-
let deferredIdentityRotation:
|
|
2802
|
+
let deferredIdentityRotation:
|
|
2803
|
+
| { event: { previousSessionFile?: string }; ctx: ExtensionContext; awaitStartup: boolean }
|
|
2804
|
+
| undefined;
|
|
2767
2805
|
let extensionShuttingDown = false;
|
|
2768
2806
|
|
|
2769
2807
|
async function ensureTelegramOwner(
|
|
@@ -2780,7 +2818,7 @@ export function createNotificationsExtension(
|
|
|
2780
2818
|
? "blocked_identity"
|
|
2781
2819
|
: "ready";
|
|
2782
2820
|
}
|
|
2783
|
-
async function
|
|
2821
|
+
async function ensureConfiguredDaemonOwners(
|
|
2784
2822
|
settings: Settings,
|
|
2785
2823
|
cfg: NotificationConfig,
|
|
2786
2824
|
cwd: string,
|
|
@@ -2789,8 +2827,7 @@ export function createNotificationsExtension(
|
|
|
2789
2827
|
if (isTelegramConfigured(cfg)) {
|
|
2790
2828
|
if ((await ensureTelegramOwner(settings, cwd, id)) === "blocked_identity") return false;
|
|
2791
2829
|
}
|
|
2792
|
-
|
|
2793
|
-
if (isSlackConfigured(cfg)) await ensureSlackDaemon(settings);
|
|
2830
|
+
await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
|
|
2794
2831
|
return true;
|
|
2795
2832
|
}
|
|
2796
2833
|
const identityControlOperations = new Set([
|
|
@@ -2953,6 +2990,16 @@ export function createNotificationsExtension(
|
|
|
2953
2990
|
return ctx.sessionMetadata?.kind !== "sub";
|
|
2954
2991
|
}
|
|
2955
2992
|
|
|
2993
|
+
function canDeliverAsync(runtime: SessionRuntime, generation: number): boolean {
|
|
2994
|
+
return (
|
|
2995
|
+
runtimes.get(runtime.id) === runtime &&
|
|
2996
|
+
!runtime.stopping &&
|
|
2997
|
+
runtime.notificationsActive &&
|
|
2998
|
+
!runtime.redact &&
|
|
2999
|
+
runtime.policyGeneration === generation
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
2956
3003
|
async function startSession(ctx: ExtensionContext): Promise<SessionStartResult> {
|
|
2957
3004
|
const id = sessionId(ctx);
|
|
2958
3005
|
const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
|
|
@@ -3006,8 +3053,6 @@ export function createNotificationsExtension(
|
|
|
3006
3053
|
const pendingInteractive = new Map<string, PendingInteractiveAsk>();
|
|
3007
3054
|
const pendingPromptCorrelations: Array<{ commandId: string; turnId: string }> = [];
|
|
3008
3055
|
const tag = sessionTag(id);
|
|
3009
|
-
const redact = cfg.redact;
|
|
3010
|
-
const verbosity = cfg.verbosity;
|
|
3011
3056
|
let runtime: SessionRuntime | undefined;
|
|
3012
3057
|
|
|
3013
3058
|
// The SDK can always answer now (interactive via the answer source, or the
|
|
@@ -3027,7 +3072,7 @@ export function createNotificationsExtension(
|
|
|
3027
3072
|
if (lifecycleRequired) return failLifecycleStartup("failed", error);
|
|
3028
3073
|
throw error;
|
|
3029
3074
|
}
|
|
3030
|
-
const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ??
|
|
3075
|
+
const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ?? true, tag);
|
|
3031
3076
|
let inboundSdkFrame: ((connectionId: string, frame: Record<string, unknown>) => void) | undefined;
|
|
3032
3077
|
const inFlightGateResolutions = new Set<Promise<void>>();
|
|
3033
3078
|
const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
|
|
@@ -3097,7 +3142,7 @@ export function createNotificationsExtension(
|
|
|
3097
3142
|
error: unknown;
|
|
3098
3143
|
terminal: boolean;
|
|
3099
3144
|
createdAt: number;
|
|
3100
|
-
|
|
3145
|
+
bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
|
|
3101
3146
|
};
|
|
3102
3147
|
const promptSubmissions = new Map<string, PromptSubmission>();
|
|
3103
3148
|
const promptTerminalTombstones = new Map<string, number>();
|
|
@@ -3129,7 +3174,7 @@ export function createNotificationsExtension(
|
|
|
3129
3174
|
};
|
|
3130
3175
|
const abandonPrompt = (submission: PromptSubmission) => {
|
|
3131
3176
|
submission.abandoned = true;
|
|
3132
|
-
submission.
|
|
3177
|
+
submission.bufferedFrames.length = 0;
|
|
3133
3178
|
};
|
|
3134
3179
|
const emitPromptLifecycle = (
|
|
3135
3180
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
@@ -3149,7 +3194,7 @@ export function createNotificationsExtension(
|
|
|
3149
3194
|
return;
|
|
3150
3195
|
}
|
|
3151
3196
|
if (!submission.acknowledged) {
|
|
3152
|
-
submission.
|
|
3197
|
+
submission.bufferedFrames.push(frame);
|
|
3153
3198
|
return;
|
|
3154
3199
|
}
|
|
3155
3200
|
try {
|
|
@@ -3160,8 +3205,31 @@ export function createNotificationsExtension(
|
|
|
3160
3205
|
}
|
|
3161
3206
|
if (submission.terminal) finalizePrompt(key, correlation);
|
|
3162
3207
|
};
|
|
3208
|
+
const emitPromptEvent = (event: AgentSessionEvent) => {
|
|
3209
|
+
if (!runtime?.activePromptCorrelation) return;
|
|
3210
|
+
cleanupPromptRecords();
|
|
3211
|
+
const correlation = runtime.activePromptCorrelation;
|
|
3212
|
+
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3213
|
+
if (!submission || submission.abandoned) return;
|
|
3214
|
+
const frame = {
|
|
3215
|
+
type: "event",
|
|
3216
|
+
kind: event.type,
|
|
3217
|
+
payload: toAgentWireEventPayload(event),
|
|
3218
|
+
...correlation,
|
|
3219
|
+
};
|
|
3220
|
+
if (!submission.acknowledged) {
|
|
3221
|
+
submission.bufferedFrames.push(frame);
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
try {
|
|
3225
|
+
runtime.server.sendTo(submission.connectionId, JSON.stringify(frame));
|
|
3226
|
+
} catch (error) {
|
|
3227
|
+
logger.warn(`sdk: correlated agent event delivery failed: ${String(error)}`);
|
|
3228
|
+
abandonPrompt(submission);
|
|
3229
|
+
}
|
|
3230
|
+
};
|
|
3163
3231
|
const flushPromptLifecycle = (key: string, submission: PromptSubmission) => {
|
|
3164
|
-
for (const frame of submission.
|
|
3232
|
+
for (const frame of submission.bufferedFrames.splice(0)) {
|
|
3165
3233
|
try {
|
|
3166
3234
|
server.sendTo(submission.connectionId, JSON.stringify(frame));
|
|
3167
3235
|
} catch (error) {
|
|
@@ -3197,7 +3265,7 @@ export function createNotificationsExtension(
|
|
|
3197
3265
|
error: undefined,
|
|
3198
3266
|
terminal: false,
|
|
3199
3267
|
createdAt: Date.now(),
|
|
3200
|
-
|
|
3268
|
+
bufferedFrames: [],
|
|
3201
3269
|
});
|
|
3202
3270
|
};
|
|
3203
3271
|
const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
|
|
@@ -3371,7 +3439,8 @@ export function createNotificationsExtension(
|
|
|
3371
3439
|
const pending = deferredIdentityRotation;
|
|
3372
3440
|
deferredIdentityRotation = undefined;
|
|
3373
3441
|
identityControlInFlight = false;
|
|
3374
|
-
if (response.ok === true && pending)
|
|
3442
|
+
if (response.ok === true && pending)
|
|
3443
|
+
await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
|
|
3375
3444
|
}
|
|
3376
3445
|
},
|
|
3377
3446
|
control: async (connectionId, frame) => {
|
|
@@ -3466,17 +3535,23 @@ export function createNotificationsExtension(
|
|
|
3466
3535
|
disableEphemeralTurns: () => {},
|
|
3467
3536
|
cancelPostmortemCleanup: () => {},
|
|
3468
3537
|
|
|
3469
|
-
redact,
|
|
3470
|
-
|
|
3471
|
-
|
|
3538
|
+
redact: true,
|
|
3539
|
+
committedRedact: true,
|
|
3540
|
+
policySuspended: true,
|
|
3541
|
+
verbosity: "lean",
|
|
3542
|
+
stream: false,
|
|
3543
|
+
policyGeneration: 0,
|
|
3472
3544
|
sessionTag: tag,
|
|
3473
3545
|
busy: false,
|
|
3474
3546
|
pendingPromptCorrelations,
|
|
3475
3547
|
activePromptCorrelation: undefined,
|
|
3476
3548
|
recordPromptTerminal,
|
|
3477
3549
|
emitPromptLifecycle,
|
|
3550
|
+
emitPromptEvent,
|
|
3478
3551
|
pendingInbound: new Set<number>(),
|
|
3479
3552
|
inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
|
|
3553
|
+
deferredGatePresentations: [],
|
|
3554
|
+
deferredInboundControls: [],
|
|
3480
3555
|
};
|
|
3481
3556
|
const initializedRuntime = runtime;
|
|
3482
3557
|
runtimes.set(id, initializedRuntime);
|
|
@@ -3501,21 +3576,26 @@ export function createNotificationsExtension(
|
|
|
3501
3576
|
};
|
|
3502
3577
|
const cleanupAbandonedStartup = async (): Promise<void> => {
|
|
3503
3578
|
try {
|
|
3504
|
-
await
|
|
3505
|
-
} catch {
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
}
|
|
3510
|
-
try {
|
|
3511
|
-
cursors.close();
|
|
3512
|
-
await revisions.close();
|
|
3513
|
-
} catch {}
|
|
3579
|
+
await stopSession(id, "session", initializedRuntime);
|
|
3580
|
+
} catch (error) {
|
|
3581
|
+
// stopSession fences the exact runtime before releasing its owners and records
|
|
3582
|
+
// the lifecycle rollback proof even when one release needs a later retry.
|
|
3583
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
3584
|
+
}
|
|
3514
3585
|
};
|
|
3515
3586
|
|
|
3516
3587
|
const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
|
|
3517
3588
|
if (!options.runEphemeralTurn) throw new Error("Ephemeral turns are unavailable.");
|
|
3518
|
-
|
|
3589
|
+
const generation = initializedRuntime.policyGeneration;
|
|
3590
|
+
if (initializedRuntime.policySuspended) throw new Error("Notification policy is provisional.");
|
|
3591
|
+
const result = await options.runEphemeralTurn(prompt.render(btwUserPrompt, { question }), signal);
|
|
3592
|
+
if (
|
|
3593
|
+
initializedRuntime.policySuspended ||
|
|
3594
|
+
initializedRuntime.policyGeneration !== generation ||
|
|
3595
|
+
runtimes.get(id) !== initializedRuntime
|
|
3596
|
+
)
|
|
3597
|
+
throw new Error("Notification policy changed during the ephemeral turn.");
|
|
3598
|
+
return result;
|
|
3519
3599
|
});
|
|
3520
3600
|
initializedRuntime.abortEphemeralTurns = () => ephemeralTurns.dispose();
|
|
3521
3601
|
initializedRuntime.disableEphemeralTurns = () => ephemeralTurns.disable();
|
|
@@ -3553,12 +3633,18 @@ export function createNotificationsExtension(
|
|
|
3553
3633
|
|
|
3554
3634
|
server.onReply((err, reply) => {
|
|
3555
3635
|
if (err || !reply) return;
|
|
3556
|
-
if (runtime?.stopping || runtimes.get(id) !== runtime) {
|
|
3636
|
+
if (runtime?.stopping || runtime?.policySuspended || runtimes.get(id) !== runtime) {
|
|
3557
3637
|
try {
|
|
3558
3638
|
server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
|
|
3559
3639
|
} catch {}
|
|
3560
3640
|
return;
|
|
3561
3641
|
}
|
|
3642
|
+
const replyGeneration = runtime.policyGeneration;
|
|
3643
|
+
const replyIsCurrent = (): boolean =>
|
|
3644
|
+
runtimes.get(id) === runtime &&
|
|
3645
|
+
!runtime.stopping &&
|
|
3646
|
+
!runtime.policySuspended &&
|
|
3647
|
+
runtime.policyGeneration === replyGeneration;
|
|
3562
3648
|
const native = server as unknown as {
|
|
3563
3649
|
resolveClaim(receiptId: string, answerJson?: string, idempotencyKey?: string): void;
|
|
3564
3650
|
closeClaimInvalid(receiptId: string, reason: string): void;
|
|
@@ -3600,6 +3686,13 @@ export function createNotificationsExtension(
|
|
|
3600
3686
|
settle(settlement: AskSettlement): Promise<AskSettlementResult> {
|
|
3601
3687
|
if (settled) return settled;
|
|
3602
3688
|
settled = Promise.resolve().then(async () => {
|
|
3689
|
+
if (!replyIsCurrent()) {
|
|
3690
|
+
try {
|
|
3691
|
+
native.closeClaimInvalid(reply.replyReceiptId, "policy_changed");
|
|
3692
|
+
} catch {}
|
|
3693
|
+
pending.fail(reply.id);
|
|
3694
|
+
return { kind: "invalid_closed" };
|
|
3695
|
+
}
|
|
3603
3696
|
if (settlement.kind === "invalid") {
|
|
3604
3697
|
try {
|
|
3605
3698
|
native.closeClaimInvalid(reply.replyReceiptId, settlement.reason);
|
|
@@ -3626,6 +3719,11 @@ export function createNotificationsExtension(
|
|
|
3626
3719
|
commitKey: `${reply.id}:${reply.idempotencyKey ?? reply.replyReceiptId}`,
|
|
3627
3720
|
deadlineAt: Date.now() + 8_000,
|
|
3628
3721
|
});
|
|
3722
|
+
if (!replyIsCurrent()) {
|
|
3723
|
+
native.closeClaimInvalid(reply.replyReceiptId, "policy_changed");
|
|
3724
|
+
pending.fail(reply.id);
|
|
3725
|
+
return { kind: "invalid_closed" };
|
|
3726
|
+
}
|
|
3629
3727
|
native.resolveClaim(
|
|
3630
3728
|
reply.replyReceiptId,
|
|
3631
3729
|
reply.answerJson,
|
|
@@ -3711,24 +3809,33 @@ export function createNotificationsExtension(
|
|
|
3711
3809
|
replyReceiptId: reply.replyReceiptId,
|
|
3712
3810
|
answerJson: reply.answerJson,
|
|
3713
3811
|
idempotencyKey: reply.idempotencyKey ?? undefined,
|
|
3714
|
-
resolveClaim: () =>
|
|
3812
|
+
resolveClaim: () => {
|
|
3813
|
+
if (!replyIsCurrent()) {
|
|
3814
|
+
native.closeClaimInvalid(reply.replyReceiptId, "policy_changed");
|
|
3815
|
+
throw new NotificationGatePolicyChangedError();
|
|
3816
|
+
}
|
|
3715
3817
|
native.resolveClaim(
|
|
3716
3818
|
reply.replyReceiptId,
|
|
3717
3819
|
reply.answerJson,
|
|
3718
3820
|
reply.idempotencyKey ?? undefined,
|
|
3719
|
-
)
|
|
3821
|
+
);
|
|
3822
|
+
},
|
|
3720
3823
|
closeClaimInvalid: reason => {
|
|
3721
3824
|
native.closeClaimInvalid(reply.replyReceiptId, reason);
|
|
3722
3825
|
gatePresentations.closeInteraction(reply.id, reason);
|
|
3723
3826
|
gatePresentations.reconcile();
|
|
3724
3827
|
},
|
|
3725
|
-
requestSelectedAck: input =>
|
|
3726
|
-
|
|
3828
|
+
requestSelectedAck: async input => {
|
|
3829
|
+
if (!replyIsCurrent()) throw new NotificationGatePolicyChangedError();
|
|
3830
|
+
const ack = await requestLiveSelectedAck(native, {
|
|
3727
3831
|
replyReceiptId: input.replyReceiptId,
|
|
3728
3832
|
actionId: input.actionId,
|
|
3729
3833
|
commitKey: input.commitKey,
|
|
3730
3834
|
deadlineAt: input.daemonDeadlineAt,
|
|
3731
|
-
})
|
|
3835
|
+
});
|
|
3836
|
+
if (!replyIsCurrent()) throw new NotificationGatePolicyChangedError();
|
|
3837
|
+
return ack;
|
|
3838
|
+
},
|
|
3732
3839
|
},
|
|
3733
3840
|
)
|
|
3734
3841
|
.catch(() => {
|
|
@@ -3773,6 +3880,26 @@ export function createNotificationsExtension(
|
|
|
3773
3880
|
messageId?: number;
|
|
3774
3881
|
reason?: string;
|
|
3775
3882
|
};
|
|
3883
|
+
const notificationOrigin = hostCapCache
|
|
3884
|
+
.get(authenticatedInbound.connectionId)
|
|
3885
|
+
?.has(ASK_SELECTED_ACK_CAPABILITY);
|
|
3886
|
+
if (runtime?.policySuspended && notificationOrigin) {
|
|
3887
|
+
if (inbound.kind === "control_command") {
|
|
3888
|
+
const frame = sdkInboundFrame(inbound.commandJson);
|
|
3889
|
+
if (frame) {
|
|
3890
|
+
const suspendedRuntime = runtime;
|
|
3891
|
+
runtime.deferredInboundControls.push(() => {
|
|
3892
|
+
if (
|
|
3893
|
+
runtimes.get(id) === suspendedRuntime &&
|
|
3894
|
+
!suspendedRuntime.stopping &&
|
|
3895
|
+
!suspendedRuntime.policySuspended
|
|
3896
|
+
)
|
|
3897
|
+
inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3776
3903
|
if (inbound.kind === "control_command") {
|
|
3777
3904
|
const frame = sdkInboundFrame(inbound.commandJson);
|
|
3778
3905
|
if (frame) {
|
|
@@ -3832,6 +3959,7 @@ export function createNotificationsExtension(
|
|
|
3832
3959
|
}
|
|
3833
3960
|
if (inbound.kind === "config_command") {
|
|
3834
3961
|
if (!runtime) return;
|
|
3962
|
+
if (runtime.policySuspended) return;
|
|
3835
3963
|
const update: {
|
|
3836
3964
|
type: "config_update";
|
|
3837
3965
|
sessionId: string;
|
|
@@ -3846,16 +3974,15 @@ export function createNotificationsExtension(
|
|
|
3846
3974
|
update.verbosity = inbound.verbosity;
|
|
3847
3975
|
}
|
|
3848
3976
|
if (typeof inbound.redact === "boolean") {
|
|
3849
|
-
|
|
3850
|
-
// bubbles (while redact is still off so the helper emits) so they never
|
|
3851
|
-
// strand permanently in "started"; subsequent detail is then suppressed.
|
|
3852
|
-
if (inbound.redact && !runtime.redact) {
|
|
3977
|
+
if (inbound.redact && !runtime.committedRedact) {
|
|
3853
3978
|
terminalizeInFlightTools(runtime, runtime.id, "unknown");
|
|
3854
3979
|
}
|
|
3980
|
+
runtime.committedRedact = inbound.redact;
|
|
3855
3981
|
runtime.redact = inbound.redact;
|
|
3856
3982
|
update.redact = inbound.redact;
|
|
3857
3983
|
}
|
|
3858
3984
|
if (update.verbosity !== undefined || update.redact !== undefined) {
|
|
3985
|
+
runtime.policyGeneration++;
|
|
3859
3986
|
try {
|
|
3860
3987
|
pushSessionFrame(runtime, update);
|
|
3861
3988
|
} catch (error) {
|
|
@@ -3907,7 +4034,7 @@ export function createNotificationsExtension(
|
|
|
3907
4034
|
}
|
|
3908
4035
|
if (notificationsEnabledForSession && settingsAvailable && settings) {
|
|
3909
4036
|
try {
|
|
3910
|
-
if (!(await
|
|
4037
|
+
if (!(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id))) {
|
|
3911
4038
|
const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
|
|
3912
4039
|
finishStartup(result);
|
|
3913
4040
|
await cleanupAbandonedStartup();
|
|
@@ -3921,10 +4048,9 @@ export function createNotificationsExtension(
|
|
|
3921
4048
|
}
|
|
3922
4049
|
}
|
|
3923
4050
|
|
|
3924
|
-
// Startup contract:
|
|
3925
|
-
//
|
|
3926
|
-
//
|
|
3927
|
-
// before broadcasting identity; late SDK consumers still recover it from
|
|
4051
|
+
// Startup contract: configured notification daemon ownership must be ready
|
|
4052
|
+
// before identity or endpoint publication. Native frames are ephemeral, so
|
|
4053
|
+
// publish identity only after readiness; late SDK consumers recover it from
|
|
3928
4054
|
// event_replay.
|
|
3929
4055
|
const identityHeader = {
|
|
3930
4056
|
type: "identity_header",
|
|
@@ -4002,9 +4128,13 @@ export function createNotificationsExtension(
|
|
|
4002
4128
|
gatePresentations,
|
|
4003
4129
|
);
|
|
4004
4130
|
runtime.disposeFileSink = registerTelegramFileSink(runtime.id, async file => {
|
|
4005
|
-
|
|
4131
|
+
const generation = runtime.policyGeneration;
|
|
4132
|
+
if (!canDeliverAsync(runtime, generation)) return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
4006
4133
|
try {
|
|
4007
|
-
const data = await fs.promises.readFile(file.path);
|
|
4134
|
+
const data = await (options.readNotificationFile ?? fs.promises.readFile)(file.path);
|
|
4135
|
+
if (!canDeliverAsync(runtime, generation)) {
|
|
4136
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
4137
|
+
}
|
|
4008
4138
|
pushFileAttachment(
|
|
4009
4139
|
runtime,
|
|
4010
4140
|
{
|
|
@@ -4065,7 +4195,11 @@ export function createNotificationsExtension(
|
|
|
4065
4195
|
logger.warn(`notifications: gate terminal controller unavailable: ${String(error)}`);
|
|
4066
4196
|
}
|
|
4067
4197
|
}
|
|
4068
|
-
|
|
4198
|
+
const presentGate = (
|
|
4199
|
+
g: Parameters<NonNullable<WorkflowGateEmitter["onGateEmitted"]>>[0] extends (gate: infer Gate) => void
|
|
4200
|
+
? Gate
|
|
4201
|
+
: never,
|
|
4202
|
+
): void => {
|
|
4069
4203
|
const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
|
|
4070
4204
|
gateOptions.set(g.gate_id, options);
|
|
4071
4205
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
@@ -4089,6 +4223,13 @@ export function createNotificationsExtension(
|
|
|
4089
4223
|
navigationLabel: stageState.navigation_label === "Next" ? "Next" : "Done",
|
|
4090
4224
|
selectedOptions: [],
|
|
4091
4225
|
});
|
|
4226
|
+
};
|
|
4227
|
+
activeRuntime.disposeGateListener = gate.onGateEmitted(g => {
|
|
4228
|
+
if (activeRuntime.policySuspended) {
|
|
4229
|
+
activeRuntime.deferredGatePresentations.push(() => presentGate(g));
|
|
4230
|
+
return;
|
|
4231
|
+
}
|
|
4232
|
+
presentGate(g);
|
|
4092
4233
|
});
|
|
4093
4234
|
if (gate.setAckRecoveryParticipant) {
|
|
4094
4235
|
const native = server as unknown as {
|
|
@@ -4097,13 +4238,19 @@ export function createNotificationsExtension(
|
|
|
4097
4238
|
): Promise<{ status: string; messageId?: number; reason?: string }>;
|
|
4098
4239
|
};
|
|
4099
4240
|
gate.setAckRecoveryParticipant({
|
|
4100
|
-
requestRecoveredAskSelectedAck: input =>
|
|
4101
|
-
|
|
4241
|
+
requestRecoveredAskSelectedAck: async input => {
|
|
4242
|
+
const generation = activeRuntime.policyGeneration;
|
|
4243
|
+
if (activeRuntime.policySuspended) return { status: "failed", reason: "cancelled" };
|
|
4244
|
+
const outcome = await requestRecoveredSelectedAck(native, {
|
|
4102
4245
|
sessionId: input.sessionId,
|
|
4103
4246
|
actionId: input.actionId,
|
|
4104
4247
|
commitKey: input.commitKey,
|
|
4105
4248
|
deadlineAt: input.deadlineAt,
|
|
4106
|
-
})
|
|
4249
|
+
});
|
|
4250
|
+
if (activeRuntime.policySuspended || activeRuntime.policyGeneration !== generation)
|
|
4251
|
+
return { status: "failed", reason: "cancelled" };
|
|
4252
|
+
return outcome;
|
|
4253
|
+
},
|
|
4107
4254
|
});
|
|
4108
4255
|
activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
|
|
4109
4256
|
}
|
|
@@ -4113,13 +4260,13 @@ export function createNotificationsExtension(
|
|
|
4113
4260
|
};
|
|
4114
4261
|
activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
|
|
4115
4262
|
if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
|
|
4116
|
-
if (notificationsEnabledForSession) initializedRuntime.enableNotifications();
|
|
4117
4263
|
finishStartup({ status: "started", runtime: initializedRuntime });
|
|
4118
4264
|
return { status: "started", runtime: initializedRuntime };
|
|
4119
4265
|
} catch (e) {
|
|
4120
4266
|
logger.warn(`notifications: failed to start server: ${String(e)}`);
|
|
4121
4267
|
const result = failLifecycleStartup("failed", e);
|
|
4122
4268
|
finishStartup(result);
|
|
4269
|
+
let suppressExtensionError = false;
|
|
4123
4270
|
let stopped = false;
|
|
4124
4271
|
try {
|
|
4125
4272
|
stopped = await stopSession(id, "session", runtime);
|
|
@@ -4129,9 +4276,10 @@ export function createNotificationsExtension(
|
|
|
4129
4276
|
// than letting it escape startSession and surface a red extension error
|
|
4130
4277
|
// through session_start / session_switch / session_branch.
|
|
4131
4278
|
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4279
|
+
suppressExtensionError = true;
|
|
4132
4280
|
}
|
|
4133
4281
|
if (!stopped) await cleanupAbandonedStartup();
|
|
4134
|
-
return { ...result, runtime };
|
|
4282
|
+
return { ...result, runtime, suppressExtensionError };
|
|
4135
4283
|
}
|
|
4136
4284
|
}
|
|
4137
4285
|
|
|
@@ -4145,25 +4293,51 @@ export function createNotificationsExtension(
|
|
|
4145
4293
|
if (!runtime || sessionId(binding.context) !== binding.sessionId || activeRuntimeId !== binding.sessionId) {
|
|
4146
4294
|
return "failed";
|
|
4147
4295
|
}
|
|
4148
|
-
runtime.enableNotifications();
|
|
4149
4296
|
return "started";
|
|
4150
4297
|
}
|
|
4151
4298
|
const runtime = runtimes.get(binding.sessionId);
|
|
4152
4299
|
if (runtime) {
|
|
4153
|
-
runtime.enableNotifications();
|
|
4154
4300
|
return "started";
|
|
4155
4301
|
}
|
|
4156
4302
|
const result = await startSession(binding.context);
|
|
4157
4303
|
return result.status === "started" || result.status === "already" ? "started" : result.status;
|
|
4158
4304
|
},
|
|
4159
4305
|
stop: async binding => await stopSession(binding.sessionId, "notifications"),
|
|
4306
|
+
refreshPolicy: (binding, policy) => {
|
|
4307
|
+
const runtime = runtimes.get(binding.sessionId);
|
|
4308
|
+
if (!runtime) return;
|
|
4309
|
+
if (policy.mode === "provisional") {
|
|
4310
|
+
runtime.policyGeneration++;
|
|
4311
|
+
runtime.policySuspended = true;
|
|
4312
|
+
runtime.redact = true;
|
|
4313
|
+
runtime.verbosity = "lean";
|
|
4314
|
+
runtime.stream = false;
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
const redactionEnabled = policy.redact && !runtime.committedRedact;
|
|
4318
|
+
runtime.policyGeneration++;
|
|
4319
|
+
runtime.committedRedact = policy.redact;
|
|
4320
|
+
runtime.policySuspended = false;
|
|
4321
|
+
runtime.redact = policy.redact;
|
|
4322
|
+
runtime.verbosity = policy.verbosity;
|
|
4323
|
+
runtime.stream = policy.stream;
|
|
4324
|
+
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "unknown");
|
|
4325
|
+
},
|
|
4326
|
+
activate: binding => {
|
|
4327
|
+
const runtime = runtimes.get(binding.sessionId);
|
|
4328
|
+
if (!runtime || runtime.stopping) return;
|
|
4329
|
+
runtime.enableNotifications();
|
|
4330
|
+
flushPendingFinal(runtime, runtime.id);
|
|
4331
|
+
for (const present of runtime.deferredGatePresentations.splice(0)) present();
|
|
4332
|
+
for (const processControl of runtime.deferredInboundControls.splice(0)) processControl();
|
|
4333
|
+
},
|
|
4160
4334
|
ensureTelegramDaemon: async binding => {
|
|
4161
4335
|
const { settings, settingsAvailable } = resolveSettings(options.settings);
|
|
4162
4336
|
if (!settingsAvailable || !settings) return "blocked_identity";
|
|
4163
4337
|
try {
|
|
4164
4338
|
return await ensureTelegramOwner(settings, binding.cwd, binding.sessionId);
|
|
4165
4339
|
} catch {
|
|
4166
|
-
return "
|
|
4340
|
+
return "failed";
|
|
4167
4341
|
}
|
|
4168
4342
|
},
|
|
4169
4343
|
};
|
|
@@ -4214,16 +4388,17 @@ export function createNotificationsExtension(
|
|
|
4214
4388
|
const result = await controller.setLocalEnabled(ctx, true);
|
|
4215
4389
|
const enabled = result.status.running && result.status.effectiveEnabled;
|
|
4216
4390
|
const rotated = sessionId(ctx) !== id;
|
|
4391
|
+
if (rotated) await stopSession(id);
|
|
4217
4392
|
const failed = result.outcome === "failed" || (!enabled && !rotated && activeRuntimeId !== id);
|
|
4218
4393
|
ctx.ui.notify(
|
|
4219
|
-
|
|
4220
|
-
? "Notifications enabled
|
|
4221
|
-
:
|
|
4222
|
-
? "Notifications
|
|
4394
|
+
rotated
|
|
4395
|
+
? "Notifications were not enabled because the active session changed during startup."
|
|
4396
|
+
: enabled
|
|
4397
|
+
? "Notifications enabled for this session."
|
|
4223
4398
|
: failed
|
|
4224
4399
|
? "Notifications failed to start for this session."
|
|
4225
4400
|
: "Notifications were not enabled because daemon ownership could not be proved.",
|
|
4226
|
-
|
|
4401
|
+
rotated ? "warning" : enabled ? "info" : failed ? "error" : "warning",
|
|
4227
4402
|
);
|
|
4228
4403
|
return;
|
|
4229
4404
|
}
|
|
@@ -4242,22 +4417,93 @@ export function createNotificationsExtension(
|
|
|
4242
4417
|
},
|
|
4243
4418
|
});
|
|
4244
4419
|
|
|
4420
|
+
const startAndReconcileSession = async (ctx: ExtensionContext): Promise<void> => {
|
|
4421
|
+
const result = await startSession(ctx);
|
|
4422
|
+
if (result.status === "started" || result.status === "already") {
|
|
4423
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
if (
|
|
4427
|
+
!lifecycleStartupCapability &&
|
|
4428
|
+
result.status === "failed" &&
|
|
4429
|
+
!extensionShuttingDown &&
|
|
4430
|
+
!result.suppressExtensionError
|
|
4431
|
+
)
|
|
4432
|
+
throw new Error(`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`);
|
|
4433
|
+
};
|
|
4434
|
+
|
|
4245
4435
|
api.on("session_start", async (_event, ctx) => {
|
|
4246
|
-
await
|
|
4247
|
-
await controller.reconcileCurrentSession(ctx);
|
|
4436
|
+
await startAndReconcileSession(ctx);
|
|
4248
4437
|
});
|
|
4249
4438
|
|
|
4250
4439
|
// A session endpoint's token and generation are authority for exactly one
|
|
4251
4440
|
// session id. `/new`, fork, and resume must all tear down A before publishing
|
|
4252
4441
|
// B. Chat implementations may preserve a topic as metadata, but it must never
|
|
4253
4442
|
// preserve A's endpoint or credentials as B's control/viewing authority.
|
|
4443
|
+
const reconcileBackgroundStartup = (
|
|
4444
|
+
id: string,
|
|
4445
|
+
ctx: ExtensionContext,
|
|
4446
|
+
startup: Promise<SessionStartResult>,
|
|
4447
|
+
): Promise<void> =>
|
|
4448
|
+
startup
|
|
4449
|
+
.then(async result => {
|
|
4450
|
+
if (
|
|
4451
|
+
result.status !== "started" ||
|
|
4452
|
+
extensionShuttingDown ||
|
|
4453
|
+
sessionId(ctx) !== id ||
|
|
4454
|
+
activeRuntimeId !== id ||
|
|
4455
|
+
!runtimes.has(id)
|
|
4456
|
+
)
|
|
4457
|
+
return;
|
|
4458
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4459
|
+
})
|
|
4460
|
+
.catch(error => logger.warn(`notifications: deferred startup reconciliation failed: ${String(error)}`));
|
|
4461
|
+
|
|
4462
|
+
const trackBranchStartup = (id: string, ctx: ExtensionContext, startup: Promise<SessionStartResult>): void => {
|
|
4463
|
+
let status: SessionStartResult["status"] = "failed";
|
|
4464
|
+
void startup.then(
|
|
4465
|
+
result => {
|
|
4466
|
+
status = result.status;
|
|
4467
|
+
},
|
|
4468
|
+
() => {},
|
|
4469
|
+
);
|
|
4470
|
+
const task = reconcileBackgroundStartup(id, ctx, startup);
|
|
4471
|
+
branchStartupTasks.add(task);
|
|
4472
|
+
void task.finally(() => {
|
|
4473
|
+
branchStartupTasks.delete(task);
|
|
4474
|
+
try {
|
|
4475
|
+
options.onBranchStartupSettled?.({ sessionId: id, status });
|
|
4476
|
+
} catch (error) {
|
|
4477
|
+
logger.warn(`notifications: branch startup receipt failed: ${String(error)}`);
|
|
4478
|
+
}
|
|
4479
|
+
});
|
|
4480
|
+
};
|
|
4481
|
+
|
|
4254
4482
|
const rotateSessionAuthority = async (
|
|
4255
4483
|
event: { previousSessionFile?: string },
|
|
4256
4484
|
ctx: ExtensionContext,
|
|
4485
|
+
awaitStartup: boolean,
|
|
4257
4486
|
): Promise<void> => {
|
|
4258
4487
|
if (extensionShuttingDown) return;
|
|
4259
4488
|
const newId = sessionId(ctx);
|
|
4260
4489
|
const prevId = activeRuntimeId ?? sessionIdFromFile(event.previousSessionFile);
|
|
4490
|
+
if (prevId === newId) {
|
|
4491
|
+
const pendingStartup = sessionStartPromises.get(newId);
|
|
4492
|
+
if (pendingStartup) {
|
|
4493
|
+
if (awaitStartup) {
|
|
4494
|
+
await pendingStartup;
|
|
4495
|
+
if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
|
|
4496
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4497
|
+
} else {
|
|
4498
|
+
trackBranchStartup(newId, ctx, pendingStartup);
|
|
4499
|
+
}
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4502
|
+
if (runtimes.has(newId)) {
|
|
4503
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4261
4507
|
if (prevId && prevId !== newId) {
|
|
4262
4508
|
controller.rekeySession(prevId, newId);
|
|
4263
4509
|
try {
|
|
@@ -4271,26 +4517,35 @@ export function createNotificationsExtension(
|
|
|
4271
4517
|
}
|
|
4272
4518
|
}
|
|
4273
4519
|
if (extensionShuttingDown) return;
|
|
4274
|
-
|
|
4275
|
-
if (
|
|
4276
|
-
await
|
|
4520
|
+
const startup = startSession(ctx);
|
|
4521
|
+
if (awaitStartup) {
|
|
4522
|
+
const result = await startup;
|
|
4523
|
+
if (!lifecycleStartupCapability && result.status === "failed")
|
|
4524
|
+
throw new Error(
|
|
4525
|
+
`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`,
|
|
4526
|
+
);
|
|
4527
|
+
if (extensionShuttingDown) {
|
|
4528
|
+
await stopSession(newId);
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4277
4532
|
return;
|
|
4278
4533
|
}
|
|
4279
|
-
|
|
4534
|
+
trackBranchStartup(newId, ctx, startup);
|
|
4280
4535
|
};
|
|
4281
4536
|
api.on("session_switch", async (event, ctx) => {
|
|
4282
4537
|
if (identityControlInFlight) {
|
|
4283
|
-
deferredIdentityRotation = { event, ctx };
|
|
4538
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: true };
|
|
4284
4539
|
return;
|
|
4285
4540
|
}
|
|
4286
|
-
await rotateSessionAuthority(event, ctx);
|
|
4541
|
+
await rotateSessionAuthority(event, ctx, true);
|
|
4287
4542
|
});
|
|
4288
4543
|
api.on("session_branch", async (event, ctx) => {
|
|
4289
4544
|
if (identityControlInFlight) {
|
|
4290
|
-
deferredIdentityRotation = { event, ctx };
|
|
4545
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: false };
|
|
4291
4546
|
return;
|
|
4292
4547
|
}
|
|
4293
|
-
await rotateSessionAuthority(event, ctx);
|
|
4548
|
+
await rotateSessionAuthority(event, ctx, false);
|
|
4294
4549
|
});
|
|
4295
4550
|
|
|
4296
4551
|
const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
|
|
@@ -4306,6 +4561,36 @@ export function createNotificationsExtension(
|
|
|
4306
4561
|
rt.inFlightTools.clear();
|
|
4307
4562
|
};
|
|
4308
4563
|
|
|
4564
|
+
const resetTurnStreamState = (rt: SessionRuntime): void => {
|
|
4565
|
+
rt.currentTurnText = undefined;
|
|
4566
|
+
rt.preAskFlushedText = undefined;
|
|
4567
|
+
rt.liveRef = undefined;
|
|
4568
|
+
rt.turnClosed = true;
|
|
4569
|
+
rt.lastLiveAt = undefined;
|
|
4570
|
+
rt.lastLiveText = undefined;
|
|
4571
|
+
};
|
|
4572
|
+
|
|
4573
|
+
const flushPendingFinal = (rt: SessionRuntime, id: string): void => {
|
|
4574
|
+
const pending = rt.pendingFinal;
|
|
4575
|
+
if (!pending) return;
|
|
4576
|
+
rt.pendingFinal = undefined;
|
|
4577
|
+
if (pending.text && rt.notificationsActive && !rt.redact) {
|
|
4578
|
+
try {
|
|
4579
|
+
pushSessionFrame(rt, {
|
|
4580
|
+
type: "turn_stream",
|
|
4581
|
+
sessionId: id,
|
|
4582
|
+
phase: "finalized",
|
|
4583
|
+
finalAnswer: true,
|
|
4584
|
+
text: pending.text,
|
|
4585
|
+
...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
|
|
4586
|
+
});
|
|
4587
|
+
} catch (error) {
|
|
4588
|
+
logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
resetTurnStreamState(rt);
|
|
4592
|
+
};
|
|
4593
|
+
|
|
4309
4594
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
4310
4595
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
4311
4596
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -4417,7 +4702,9 @@ export function createNotificationsExtension(
|
|
|
4417
4702
|
const model = (ctx as { getModel?: () => { id?: string } | undefined }).getModel?.();
|
|
4418
4703
|
const tokenUsage = usage && usage.tokens != null ? `${usage.tokens}/${usage.contextWindow}` : undefined;
|
|
4419
4704
|
const modelId = model?.id;
|
|
4420
|
-
|
|
4705
|
+
const generation = rt.policyGeneration;
|
|
4706
|
+
void (options.readNotificationDiffStat ?? readGitDiffStat)(ctx.cwd).then(diff => {
|
|
4707
|
+
if (!canDeliverAsync(rt, generation)) return;
|
|
4421
4708
|
const cwd = compactCwd(ctx.cwd);
|
|
4422
4709
|
if (!diff && !tokenUsage && !modelId && !cwd) return;
|
|
4423
4710
|
try {
|
|
@@ -4445,7 +4732,7 @@ export function createNotificationsExtension(
|
|
|
4445
4732
|
// Push the in-flight turn's assistant text as a finalized turn_stream, deduped
|
|
4446
4733
|
// against what was already flushed for this turn (the pre-ask lead-in).
|
|
4447
4734
|
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
|
|
4448
|
-
if (!text || text === rt.preAskFlushedText || !rt.notificationsActive) return;
|
|
4735
|
+
if (!text || text === rt.preAskFlushedText || !rt.notificationsActive || rt.policySuspended) return;
|
|
4449
4736
|
rt.preAskFlushedText = text;
|
|
4450
4737
|
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
4451
4738
|
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
@@ -4575,18 +4862,20 @@ export function createNotificationsExtension(
|
|
|
4575
4862
|
const id = sessionId(ctx);
|
|
4576
4863
|
const rt = runtimes.get(id);
|
|
4577
4864
|
if (!rt?.notificationsActive) return;
|
|
4578
|
-
const text = rt.
|
|
4865
|
+
const text = rt.policySuspended
|
|
4866
|
+
? rt.committedRedact
|
|
4867
|
+
? undefined
|
|
4868
|
+
: summaryFromMessage(event.message, turnTextMax())
|
|
4869
|
+
: rt.redact
|
|
4870
|
+
? undefined
|
|
4871
|
+
: summaryFromMessage(event.message, turnTextMax());
|
|
4872
|
+
if (rt.policySuspended) {
|
|
4873
|
+
rt.pendingFinal = { text, messageRef: rt.liveRef };
|
|
4874
|
+
rt.turnClosed = true;
|
|
4875
|
+
return;
|
|
4876
|
+
}
|
|
4579
4877
|
if (text) flushTurnText(rt, id, text, true);
|
|
4580
|
-
|
|
4581
|
-
// turn with identical text is not falsely deduped.
|
|
4582
|
-
rt.currentTurnText = undefined;
|
|
4583
|
-
rt.preAskFlushedText = undefined;
|
|
4584
|
-
rt.liveRef = undefined;
|
|
4585
|
-
// Close the live-stream window: any message_update queued after turn_end is
|
|
4586
|
-
// dropped so it can never emit a stale live edit past the finalized turn.
|
|
4587
|
-
rt.turnClosed = true;
|
|
4588
|
-
rt.lastLiveAt = undefined;
|
|
4589
|
-
rt.lastLiveText = undefined;
|
|
4878
|
+
resetTurnStreamState(rt);
|
|
4590
4879
|
});
|
|
4591
4880
|
|
|
4592
4881
|
// Live streaming (opt-in): push throttled in-progress assistant text as
|
|
@@ -4596,6 +4885,7 @@ export function createNotificationsExtension(
|
|
|
4596
4885
|
api.on("message_update", (event, ctx) => {
|
|
4597
4886
|
const id = sessionId(ctx);
|
|
4598
4887
|
const rt = runtimes.get(id);
|
|
4888
|
+
rt?.emitPromptEvent(event);
|
|
4599
4889
|
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
|
|
4600
4890
|
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
4601
4891
|
if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
@@ -4619,6 +4909,7 @@ export function createNotificationsExtension(
|
|
|
4619
4909
|
api.on("message_end", (event, ctx) => {
|
|
4620
4910
|
const id = sessionId(ctx);
|
|
4621
4911
|
const rt = runtimes.get(id);
|
|
4912
|
+
rt?.emitPromptEvent(event);
|
|
4622
4913
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4623
4914
|
// Capture the in-flight ASSISTANT text here (message_end is on the awaited
|
|
4624
4915
|
// extension path and ordered before tool_execution_start) so the pre-ask
|
|
@@ -4647,6 +4938,7 @@ export function createNotificationsExtension(
|
|
|
4647
4938
|
extensionShuttingDown = true;
|
|
4648
4939
|
identityControlInFlight = false;
|
|
4649
4940
|
deferredIdentityRotation = undefined;
|
|
4941
|
+
await Promise.allSettled([...branchStartupTasks]);
|
|
4650
4942
|
const id = sessionId(ctx);
|
|
4651
4943
|
const rt = runtimes.get(id);
|
|
4652
4944
|
if (rt) terminalizeInFlightTools(rt, id, "unknown");
|