@gajae-code/coding-agent 0.11.4 → 0.11.6
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 +31 -1
- 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 +42 -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 +11 -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 +98 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/session/yield-queue.d.ts +8 -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 +3 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/commands/sdk.ts +7 -2
- package/src/config/settings-schema.ts +31 -1
- 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 +8 -7
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +36 -12
- 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/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- 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/runtime-init.ts +16 -2
- 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 +538 -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 +388 -94
- 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 +11 -3
- package/src/sdk/bus/telegram-daemon-control.ts +312 -76
- package/src/sdk/bus/telegram-daemon.ts +1916 -285
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -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 +1447 -852
- 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 +1121 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +36 -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-answer-registry.ts +15 -1
- 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
|
}
|
|
@@ -1912,8 +1926,10 @@ function sdkControlSurface(
|
|
|
1912
1926
|
}
|
|
1913
1927
|
return "unknown";
|
|
1914
1928
|
};
|
|
1915
|
-
const sendSteer = (text: string) => {
|
|
1916
|
-
|
|
1929
|
+
const sendSteer = async (text: string) => {
|
|
1930
|
+
// Await admission so a rejection (e.g. handoff in progress) surfaces as a
|
|
1931
|
+
// control error instead of a false `accepted: true`.
|
|
1932
|
+
await api.sendUserMessage(text, { deliverAs: "steer" });
|
|
1917
1933
|
return { commandId: crypto.randomUUID(), accepted: true };
|
|
1918
1934
|
};
|
|
1919
1935
|
const resolveModel = (id: string) => {
|
|
@@ -2728,6 +2744,22 @@ function sdkInboundFrame(commandJson: string | undefined): Record<string, unknow
|
|
|
2728
2744
|
return undefined;
|
|
2729
2745
|
}
|
|
2730
2746
|
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Ensures every configured chat-provider daemon is ready before the SDK
|
|
2749
|
+
* publishes session identity. A rejected ensure is startup-fatal: presenting
|
|
2750
|
+
* an identity for a transport that never became available is false success.
|
|
2751
|
+
*/
|
|
2752
|
+
export async function ensureConfiguredProviderDaemons(
|
|
2753
|
+
settings: Settings,
|
|
2754
|
+
cfg: NotificationConfig,
|
|
2755
|
+
ensureProviderDaemon: (provider: "discord" | "slack", settings: Settings) => Promise<unknown> = (
|
|
2756
|
+
provider,
|
|
2757
|
+
configuredSettings,
|
|
2758
|
+
) => (provider === "discord" ? ensureDiscordDaemon(configuredSettings) : ensureSlackDaemon(configuredSettings)),
|
|
2759
|
+
): Promise<void> {
|
|
2760
|
+
if (isDiscordConfigured(cfg)) await ensureProviderDaemon("discord", settings);
|
|
2761
|
+
if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
|
|
2762
|
+
}
|
|
2731
2763
|
|
|
2732
2764
|
export function createNotificationsExtension(
|
|
2733
2765
|
api: ExtensionAPI,
|
|
@@ -2738,6 +2770,7 @@ export function createNotificationsExtension(
|
|
|
2738
2770
|
cwd: string;
|
|
2739
2771
|
sessionId: string;
|
|
2740
2772
|
}) => Promise<EnsureDaemonResult>;
|
|
2773
|
+
ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<unknown>;
|
|
2741
2774
|
/** Suppress auto-delivery for a GJC-spawned child under `sessionScope=primary`. */
|
|
2742
2775
|
spawnedByGjc?: boolean;
|
|
2743
2776
|
controller?: NotificationSessionController;
|
|
@@ -2746,6 +2779,10 @@ export function createNotificationsExtension(
|
|
|
2746
2779
|
|
|
2747
2780
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2748
2781
|
runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2782
|
+
/** Observes settlement of optional session-branch startup after reconciliation completes. */
|
|
2783
|
+
onBranchStartupSettled?: (receipt: { sessionId: string; status: SessionStartResult["status"] }) => void;
|
|
2784
|
+
readNotificationFile?: (path: string) => Promise<Buffer>;
|
|
2785
|
+
readNotificationDiffStat?: (cwd: string) => Promise<string | undefined>;
|
|
2749
2786
|
} = {},
|
|
2750
2787
|
): void {
|
|
2751
2788
|
const lifecycleStartupCapability = lifecycleStartupCapabilityForApi(api);
|
|
@@ -2761,9 +2798,12 @@ export function createNotificationsExtension(
|
|
|
2761
2798
|
// exact runtime object retains authority for an explicit idempotent retry.
|
|
2762
2799
|
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2763
2800
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2801
|
+
const branchStartupTasks = new Set<Promise<void>>();
|
|
2764
2802
|
let activeRuntimeId: string | undefined;
|
|
2765
2803
|
let identityControlInFlight = false;
|
|
2766
|
-
let deferredIdentityRotation:
|
|
2804
|
+
let deferredIdentityRotation:
|
|
2805
|
+
| { event: { previousSessionFile?: string }; ctx: ExtensionContext; awaitStartup: boolean }
|
|
2806
|
+
| undefined;
|
|
2767
2807
|
let extensionShuttingDown = false;
|
|
2768
2808
|
|
|
2769
2809
|
async function ensureTelegramOwner(
|
|
@@ -2780,7 +2820,7 @@ export function createNotificationsExtension(
|
|
|
2780
2820
|
? "blocked_identity"
|
|
2781
2821
|
: "ready";
|
|
2782
2822
|
}
|
|
2783
|
-
async function
|
|
2823
|
+
async function ensureConfiguredDaemonOwners(
|
|
2784
2824
|
settings: Settings,
|
|
2785
2825
|
cfg: NotificationConfig,
|
|
2786
2826
|
cwd: string,
|
|
@@ -2789,8 +2829,7 @@ export function createNotificationsExtension(
|
|
|
2789
2829
|
if (isTelegramConfigured(cfg)) {
|
|
2790
2830
|
if ((await ensureTelegramOwner(settings, cwd, id)) === "blocked_identity") return false;
|
|
2791
2831
|
}
|
|
2792
|
-
|
|
2793
|
-
if (isSlackConfigured(cfg)) await ensureSlackDaemon(settings);
|
|
2832
|
+
await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
|
|
2794
2833
|
return true;
|
|
2795
2834
|
}
|
|
2796
2835
|
const identityControlOperations = new Set([
|
|
@@ -2953,6 +2992,16 @@ export function createNotificationsExtension(
|
|
|
2953
2992
|
return ctx.sessionMetadata?.kind !== "sub";
|
|
2954
2993
|
}
|
|
2955
2994
|
|
|
2995
|
+
function canDeliverAsync(runtime: SessionRuntime, generation: number): boolean {
|
|
2996
|
+
return (
|
|
2997
|
+
runtimes.get(runtime.id) === runtime &&
|
|
2998
|
+
!runtime.stopping &&
|
|
2999
|
+
runtime.notificationsActive &&
|
|
3000
|
+
!runtime.redact &&
|
|
3001
|
+
runtime.policyGeneration === generation
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
|
|
2956
3005
|
async function startSession(ctx: ExtensionContext): Promise<SessionStartResult> {
|
|
2957
3006
|
const id = sessionId(ctx);
|
|
2958
3007
|
const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
|
|
@@ -3006,8 +3055,6 @@ export function createNotificationsExtension(
|
|
|
3006
3055
|
const pendingInteractive = new Map<string, PendingInteractiveAsk>();
|
|
3007
3056
|
const pendingPromptCorrelations: Array<{ commandId: string; turnId: string }> = [];
|
|
3008
3057
|
const tag = sessionTag(id);
|
|
3009
|
-
const redact = cfg.redact;
|
|
3010
|
-
const verbosity = cfg.verbosity;
|
|
3011
3058
|
let runtime: SessionRuntime | undefined;
|
|
3012
3059
|
|
|
3013
3060
|
// The SDK can always answer now (interactive via the answer source, or the
|
|
@@ -3027,7 +3074,7 @@ export function createNotificationsExtension(
|
|
|
3027
3074
|
if (lifecycleRequired) return failLifecycleStartup("failed", error);
|
|
3028
3075
|
throw error;
|
|
3029
3076
|
}
|
|
3030
|
-
const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ??
|
|
3077
|
+
const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ?? true, tag);
|
|
3031
3078
|
let inboundSdkFrame: ((connectionId: string, frame: Record<string, unknown>) => void) | undefined;
|
|
3032
3079
|
const inFlightGateResolutions = new Set<Promise<void>>();
|
|
3033
3080
|
const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
|
|
@@ -3097,7 +3144,7 @@ export function createNotificationsExtension(
|
|
|
3097
3144
|
error: unknown;
|
|
3098
3145
|
terminal: boolean;
|
|
3099
3146
|
createdAt: number;
|
|
3100
|
-
|
|
3147
|
+
bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
|
|
3101
3148
|
};
|
|
3102
3149
|
const promptSubmissions = new Map<string, PromptSubmission>();
|
|
3103
3150
|
const promptTerminalTombstones = new Map<string, number>();
|
|
@@ -3129,7 +3176,7 @@ export function createNotificationsExtension(
|
|
|
3129
3176
|
};
|
|
3130
3177
|
const abandonPrompt = (submission: PromptSubmission) => {
|
|
3131
3178
|
submission.abandoned = true;
|
|
3132
|
-
submission.
|
|
3179
|
+
submission.bufferedFrames.length = 0;
|
|
3133
3180
|
};
|
|
3134
3181
|
const emitPromptLifecycle = (
|
|
3135
3182
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
@@ -3149,7 +3196,7 @@ export function createNotificationsExtension(
|
|
|
3149
3196
|
return;
|
|
3150
3197
|
}
|
|
3151
3198
|
if (!submission.acknowledged) {
|
|
3152
|
-
submission.
|
|
3199
|
+
submission.bufferedFrames.push(frame);
|
|
3153
3200
|
return;
|
|
3154
3201
|
}
|
|
3155
3202
|
try {
|
|
@@ -3160,8 +3207,31 @@ export function createNotificationsExtension(
|
|
|
3160
3207
|
}
|
|
3161
3208
|
if (submission.terminal) finalizePrompt(key, correlation);
|
|
3162
3209
|
};
|
|
3210
|
+
const emitPromptEvent = (event: AgentSessionEvent) => {
|
|
3211
|
+
if (!runtime?.activePromptCorrelation) return;
|
|
3212
|
+
cleanupPromptRecords();
|
|
3213
|
+
const correlation = runtime.activePromptCorrelation;
|
|
3214
|
+
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3215
|
+
if (!submission || submission.abandoned) return;
|
|
3216
|
+
const frame = {
|
|
3217
|
+
type: "event",
|
|
3218
|
+
kind: event.type,
|
|
3219
|
+
payload: toAgentWireEventPayload(event),
|
|
3220
|
+
...correlation,
|
|
3221
|
+
};
|
|
3222
|
+
if (!submission.acknowledged) {
|
|
3223
|
+
submission.bufferedFrames.push(frame);
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
try {
|
|
3227
|
+
runtime.server.sendTo(submission.connectionId, JSON.stringify(frame));
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
logger.warn(`sdk: correlated agent event delivery failed: ${String(error)}`);
|
|
3230
|
+
abandonPrompt(submission);
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3163
3233
|
const flushPromptLifecycle = (key: string, submission: PromptSubmission) => {
|
|
3164
|
-
for (const frame of submission.
|
|
3234
|
+
for (const frame of submission.bufferedFrames.splice(0)) {
|
|
3165
3235
|
try {
|
|
3166
3236
|
server.sendTo(submission.connectionId, JSON.stringify(frame));
|
|
3167
3237
|
} catch (error) {
|
|
@@ -3197,7 +3267,7 @@ export function createNotificationsExtension(
|
|
|
3197
3267
|
error: undefined,
|
|
3198
3268
|
terminal: false,
|
|
3199
3269
|
createdAt: Date.now(),
|
|
3200
|
-
|
|
3270
|
+
bufferedFrames: [],
|
|
3201
3271
|
});
|
|
3202
3272
|
};
|
|
3203
3273
|
const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
|
|
@@ -3371,7 +3441,8 @@ export function createNotificationsExtension(
|
|
|
3371
3441
|
const pending = deferredIdentityRotation;
|
|
3372
3442
|
deferredIdentityRotation = undefined;
|
|
3373
3443
|
identityControlInFlight = false;
|
|
3374
|
-
if (response.ok === true && pending)
|
|
3444
|
+
if (response.ok === true && pending)
|
|
3445
|
+
await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
|
|
3375
3446
|
}
|
|
3376
3447
|
},
|
|
3377
3448
|
control: async (connectionId, frame) => {
|
|
@@ -3466,17 +3537,23 @@ export function createNotificationsExtension(
|
|
|
3466
3537
|
disableEphemeralTurns: () => {},
|
|
3467
3538
|
cancelPostmortemCleanup: () => {},
|
|
3468
3539
|
|
|
3469
|
-
redact,
|
|
3470
|
-
|
|
3471
|
-
|
|
3540
|
+
redact: true,
|
|
3541
|
+
committedRedact: true,
|
|
3542
|
+
policySuspended: true,
|
|
3543
|
+
verbosity: "lean",
|
|
3544
|
+
stream: false,
|
|
3545
|
+
policyGeneration: 0,
|
|
3472
3546
|
sessionTag: tag,
|
|
3473
3547
|
busy: false,
|
|
3474
3548
|
pendingPromptCorrelations,
|
|
3475
3549
|
activePromptCorrelation: undefined,
|
|
3476
3550
|
recordPromptTerminal,
|
|
3477
3551
|
emitPromptLifecycle,
|
|
3552
|
+
emitPromptEvent,
|
|
3478
3553
|
pendingInbound: new Set<number>(),
|
|
3479
3554
|
inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
|
|
3555
|
+
deferredGatePresentations: [],
|
|
3556
|
+
deferredInboundControls: [],
|
|
3480
3557
|
};
|
|
3481
3558
|
const initializedRuntime = runtime;
|
|
3482
3559
|
runtimes.set(id, initializedRuntime);
|
|
@@ -3501,21 +3578,26 @@ export function createNotificationsExtension(
|
|
|
3501
3578
|
};
|
|
3502
3579
|
const cleanupAbandonedStartup = async (): Promise<void> => {
|
|
3503
3580
|
try {
|
|
3504
|
-
await
|
|
3505
|
-
} catch {
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
}
|
|
3510
|
-
try {
|
|
3511
|
-
cursors.close();
|
|
3512
|
-
await revisions.close();
|
|
3513
|
-
} catch {}
|
|
3581
|
+
await stopSession(id, "session", initializedRuntime);
|
|
3582
|
+
} catch (error) {
|
|
3583
|
+
// stopSession fences the exact runtime before releasing its owners and records
|
|
3584
|
+
// the lifecycle rollback proof even when one release needs a later retry.
|
|
3585
|
+
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
3586
|
+
}
|
|
3514
3587
|
};
|
|
3515
3588
|
|
|
3516
3589
|
const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
|
|
3517
3590
|
if (!options.runEphemeralTurn) throw new Error("Ephemeral turns are unavailable.");
|
|
3518
|
-
|
|
3591
|
+
const generation = initializedRuntime.policyGeneration;
|
|
3592
|
+
if (initializedRuntime.policySuspended) throw new Error("Notification policy is provisional.");
|
|
3593
|
+
const result = await options.runEphemeralTurn(prompt.render(btwUserPrompt, { question }), signal);
|
|
3594
|
+
if (
|
|
3595
|
+
initializedRuntime.policySuspended ||
|
|
3596
|
+
initializedRuntime.policyGeneration !== generation ||
|
|
3597
|
+
runtimes.get(id) !== initializedRuntime
|
|
3598
|
+
)
|
|
3599
|
+
throw new Error("Notification policy changed during the ephemeral turn.");
|
|
3600
|
+
return result;
|
|
3519
3601
|
});
|
|
3520
3602
|
initializedRuntime.abortEphemeralTurns = () => ephemeralTurns.dispose();
|
|
3521
3603
|
initializedRuntime.disableEphemeralTurns = () => ephemeralTurns.disable();
|
|
@@ -3553,12 +3635,18 @@ export function createNotificationsExtension(
|
|
|
3553
3635
|
|
|
3554
3636
|
server.onReply((err, reply) => {
|
|
3555
3637
|
if (err || !reply) return;
|
|
3556
|
-
if (runtime?.stopping || runtimes.get(id) !== runtime) {
|
|
3638
|
+
if (runtime?.stopping || runtime?.policySuspended || runtimes.get(id) !== runtime) {
|
|
3557
3639
|
try {
|
|
3558
3640
|
server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
|
|
3559
3641
|
} catch {}
|
|
3560
3642
|
return;
|
|
3561
3643
|
}
|
|
3644
|
+
const replyGeneration = runtime.policyGeneration;
|
|
3645
|
+
const replyIsCurrent = (): boolean =>
|
|
3646
|
+
runtimes.get(id) === runtime &&
|
|
3647
|
+
!runtime.stopping &&
|
|
3648
|
+
!runtime.policySuspended &&
|
|
3649
|
+
runtime.policyGeneration === replyGeneration;
|
|
3562
3650
|
const native = server as unknown as {
|
|
3563
3651
|
resolveClaim(receiptId: string, answerJson?: string, idempotencyKey?: string): void;
|
|
3564
3652
|
closeClaimInvalid(receiptId: string, reason: string): void;
|
|
@@ -3600,6 +3688,13 @@ export function createNotificationsExtension(
|
|
|
3600
3688
|
settle(settlement: AskSettlement): Promise<AskSettlementResult> {
|
|
3601
3689
|
if (settled) return settled;
|
|
3602
3690
|
settled = Promise.resolve().then(async () => {
|
|
3691
|
+
if (!replyIsCurrent()) {
|
|
3692
|
+
try {
|
|
3693
|
+
native.closeClaimInvalid(reply.replyReceiptId, "policy_changed");
|
|
3694
|
+
} catch {}
|
|
3695
|
+
pending.fail(reply.id);
|
|
3696
|
+
return { kind: "invalid_closed" };
|
|
3697
|
+
}
|
|
3603
3698
|
if (settlement.kind === "invalid") {
|
|
3604
3699
|
try {
|
|
3605
3700
|
native.closeClaimInvalid(reply.replyReceiptId, settlement.reason);
|
|
@@ -3626,6 +3721,11 @@ export function createNotificationsExtension(
|
|
|
3626
3721
|
commitKey: `${reply.id}:${reply.idempotencyKey ?? reply.replyReceiptId}`,
|
|
3627
3722
|
deadlineAt: Date.now() + 8_000,
|
|
3628
3723
|
});
|
|
3724
|
+
if (!replyIsCurrent()) {
|
|
3725
|
+
native.closeClaimInvalid(reply.replyReceiptId, "policy_changed");
|
|
3726
|
+
pending.fail(reply.id);
|
|
3727
|
+
return { kind: "invalid_closed" };
|
|
3728
|
+
}
|
|
3629
3729
|
native.resolveClaim(
|
|
3630
3730
|
reply.replyReceiptId,
|
|
3631
3731
|
reply.answerJson,
|
|
@@ -3711,24 +3811,33 @@ export function createNotificationsExtension(
|
|
|
3711
3811
|
replyReceiptId: reply.replyReceiptId,
|
|
3712
3812
|
answerJson: reply.answerJson,
|
|
3713
3813
|
idempotencyKey: reply.idempotencyKey ?? undefined,
|
|
3714
|
-
resolveClaim: () =>
|
|
3814
|
+
resolveClaim: () => {
|
|
3815
|
+
if (!replyIsCurrent()) {
|
|
3816
|
+
native.closeClaimInvalid(reply.replyReceiptId, "policy_changed");
|
|
3817
|
+
throw new NotificationGatePolicyChangedError();
|
|
3818
|
+
}
|
|
3715
3819
|
native.resolveClaim(
|
|
3716
3820
|
reply.replyReceiptId,
|
|
3717
3821
|
reply.answerJson,
|
|
3718
3822
|
reply.idempotencyKey ?? undefined,
|
|
3719
|
-
)
|
|
3823
|
+
);
|
|
3824
|
+
},
|
|
3720
3825
|
closeClaimInvalid: reason => {
|
|
3721
3826
|
native.closeClaimInvalid(reply.replyReceiptId, reason);
|
|
3722
3827
|
gatePresentations.closeInteraction(reply.id, reason);
|
|
3723
3828
|
gatePresentations.reconcile();
|
|
3724
3829
|
},
|
|
3725
|
-
requestSelectedAck: input =>
|
|
3726
|
-
|
|
3830
|
+
requestSelectedAck: async input => {
|
|
3831
|
+
if (!replyIsCurrent()) throw new NotificationGatePolicyChangedError();
|
|
3832
|
+
const ack = await requestLiveSelectedAck(native, {
|
|
3727
3833
|
replyReceiptId: input.replyReceiptId,
|
|
3728
3834
|
actionId: input.actionId,
|
|
3729
3835
|
commitKey: input.commitKey,
|
|
3730
3836
|
deadlineAt: input.daemonDeadlineAt,
|
|
3731
|
-
})
|
|
3837
|
+
});
|
|
3838
|
+
if (!replyIsCurrent()) throw new NotificationGatePolicyChangedError();
|
|
3839
|
+
return ack;
|
|
3840
|
+
},
|
|
3732
3841
|
},
|
|
3733
3842
|
)
|
|
3734
3843
|
.catch(() => {
|
|
@@ -3773,6 +3882,26 @@ export function createNotificationsExtension(
|
|
|
3773
3882
|
messageId?: number;
|
|
3774
3883
|
reason?: string;
|
|
3775
3884
|
};
|
|
3885
|
+
const notificationOrigin = hostCapCache
|
|
3886
|
+
.get(authenticatedInbound.connectionId)
|
|
3887
|
+
?.has(ASK_SELECTED_ACK_CAPABILITY);
|
|
3888
|
+
if (runtime?.policySuspended && notificationOrigin) {
|
|
3889
|
+
if (inbound.kind === "control_command") {
|
|
3890
|
+
const frame = sdkInboundFrame(inbound.commandJson);
|
|
3891
|
+
if (frame) {
|
|
3892
|
+
const suspendedRuntime = runtime;
|
|
3893
|
+
runtime.deferredInboundControls.push(() => {
|
|
3894
|
+
if (
|
|
3895
|
+
runtimes.get(id) === suspendedRuntime &&
|
|
3896
|
+
!suspendedRuntime.stopping &&
|
|
3897
|
+
!suspendedRuntime.policySuspended
|
|
3898
|
+
)
|
|
3899
|
+
inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3776
3905
|
if (inbound.kind === "control_command") {
|
|
3777
3906
|
const frame = sdkInboundFrame(inbound.commandJson);
|
|
3778
3907
|
if (frame) {
|
|
@@ -3832,6 +3961,7 @@ export function createNotificationsExtension(
|
|
|
3832
3961
|
}
|
|
3833
3962
|
if (inbound.kind === "config_command") {
|
|
3834
3963
|
if (!runtime) return;
|
|
3964
|
+
if (runtime.policySuspended) return;
|
|
3835
3965
|
const update: {
|
|
3836
3966
|
type: "config_update";
|
|
3837
3967
|
sessionId: string;
|
|
@@ -3846,16 +3976,15 @@ export function createNotificationsExtension(
|
|
|
3846
3976
|
update.verbosity = inbound.verbosity;
|
|
3847
3977
|
}
|
|
3848
3978
|
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) {
|
|
3979
|
+
if (inbound.redact && !runtime.committedRedact) {
|
|
3853
3980
|
terminalizeInFlightTools(runtime, runtime.id, "unknown");
|
|
3854
3981
|
}
|
|
3982
|
+
runtime.committedRedact = inbound.redact;
|
|
3855
3983
|
runtime.redact = inbound.redact;
|
|
3856
3984
|
update.redact = inbound.redact;
|
|
3857
3985
|
}
|
|
3858
3986
|
if (update.verbosity !== undefined || update.redact !== undefined) {
|
|
3987
|
+
runtime.policyGeneration++;
|
|
3859
3988
|
try {
|
|
3860
3989
|
pushSessionFrame(runtime, update);
|
|
3861
3990
|
} catch (error) {
|
|
@@ -3907,7 +4036,7 @@ export function createNotificationsExtension(
|
|
|
3907
4036
|
}
|
|
3908
4037
|
if (notificationsEnabledForSession && settingsAvailable && settings) {
|
|
3909
4038
|
try {
|
|
3910
|
-
if (!(await
|
|
4039
|
+
if (!(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id))) {
|
|
3911
4040
|
const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
|
|
3912
4041
|
finishStartup(result);
|
|
3913
4042
|
await cleanupAbandonedStartup();
|
|
@@ -3921,10 +4050,9 @@ export function createNotificationsExtension(
|
|
|
3921
4050
|
}
|
|
3922
4051
|
}
|
|
3923
4052
|
|
|
3924
|
-
// Startup contract:
|
|
3925
|
-
//
|
|
3926
|
-
//
|
|
3927
|
-
// before broadcasting identity; late SDK consumers still recover it from
|
|
4053
|
+
// Startup contract: configured notification daemon ownership must be ready
|
|
4054
|
+
// before identity or endpoint publication. Native frames are ephemeral, so
|
|
4055
|
+
// publish identity only after readiness; late SDK consumers recover it from
|
|
3928
4056
|
// event_replay.
|
|
3929
4057
|
const identityHeader = {
|
|
3930
4058
|
type: "identity_header",
|
|
@@ -4002,9 +4130,13 @@ export function createNotificationsExtension(
|
|
|
4002
4130
|
gatePresentations,
|
|
4003
4131
|
);
|
|
4004
4132
|
runtime.disposeFileSink = registerTelegramFileSink(runtime.id, async file => {
|
|
4005
|
-
|
|
4133
|
+
const generation = runtime.policyGeneration;
|
|
4134
|
+
if (!canDeliverAsync(runtime, generation)) return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
4006
4135
|
try {
|
|
4007
|
-
const data = await fs.promises.readFile(file.path);
|
|
4136
|
+
const data = await (options.readNotificationFile ?? fs.promises.readFile)(file.path);
|
|
4137
|
+
if (!canDeliverAsync(runtime, generation)) {
|
|
4138
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
4139
|
+
}
|
|
4008
4140
|
pushFileAttachment(
|
|
4009
4141
|
runtime,
|
|
4010
4142
|
{
|
|
@@ -4065,7 +4197,11 @@ export function createNotificationsExtension(
|
|
|
4065
4197
|
logger.warn(`notifications: gate terminal controller unavailable: ${String(error)}`);
|
|
4066
4198
|
}
|
|
4067
4199
|
}
|
|
4068
|
-
|
|
4200
|
+
const presentGate = (
|
|
4201
|
+
g: Parameters<NonNullable<WorkflowGateEmitter["onGateEmitted"]>>[0] extends (gate: infer Gate) => void
|
|
4202
|
+
? Gate
|
|
4203
|
+
: never,
|
|
4204
|
+
): void => {
|
|
4069
4205
|
const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
|
|
4070
4206
|
gateOptions.set(g.gate_id, options);
|
|
4071
4207
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
@@ -4089,6 +4225,13 @@ export function createNotificationsExtension(
|
|
|
4089
4225
|
navigationLabel: stageState.navigation_label === "Next" ? "Next" : "Done",
|
|
4090
4226
|
selectedOptions: [],
|
|
4091
4227
|
});
|
|
4228
|
+
};
|
|
4229
|
+
activeRuntime.disposeGateListener = gate.onGateEmitted(g => {
|
|
4230
|
+
if (activeRuntime.policySuspended) {
|
|
4231
|
+
activeRuntime.deferredGatePresentations.push(() => presentGate(g));
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4234
|
+
presentGate(g);
|
|
4092
4235
|
});
|
|
4093
4236
|
if (gate.setAckRecoveryParticipant) {
|
|
4094
4237
|
const native = server as unknown as {
|
|
@@ -4097,13 +4240,19 @@ export function createNotificationsExtension(
|
|
|
4097
4240
|
): Promise<{ status: string; messageId?: number; reason?: string }>;
|
|
4098
4241
|
};
|
|
4099
4242
|
gate.setAckRecoveryParticipant({
|
|
4100
|
-
requestRecoveredAskSelectedAck: input =>
|
|
4101
|
-
|
|
4243
|
+
requestRecoveredAskSelectedAck: async input => {
|
|
4244
|
+
const generation = activeRuntime.policyGeneration;
|
|
4245
|
+
if (activeRuntime.policySuspended) return { status: "failed", reason: "cancelled" };
|
|
4246
|
+
const outcome = await requestRecoveredSelectedAck(native, {
|
|
4102
4247
|
sessionId: input.sessionId,
|
|
4103
4248
|
actionId: input.actionId,
|
|
4104
4249
|
commitKey: input.commitKey,
|
|
4105
4250
|
deadlineAt: input.deadlineAt,
|
|
4106
|
-
})
|
|
4251
|
+
});
|
|
4252
|
+
if (activeRuntime.policySuspended || activeRuntime.policyGeneration !== generation)
|
|
4253
|
+
return { status: "failed", reason: "cancelled" };
|
|
4254
|
+
return outcome;
|
|
4255
|
+
},
|
|
4107
4256
|
});
|
|
4108
4257
|
activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
|
|
4109
4258
|
}
|
|
@@ -4113,13 +4262,13 @@ export function createNotificationsExtension(
|
|
|
4113
4262
|
};
|
|
4114
4263
|
activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
|
|
4115
4264
|
if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
|
|
4116
|
-
if (notificationsEnabledForSession) initializedRuntime.enableNotifications();
|
|
4117
4265
|
finishStartup({ status: "started", runtime: initializedRuntime });
|
|
4118
4266
|
return { status: "started", runtime: initializedRuntime };
|
|
4119
4267
|
} catch (e) {
|
|
4120
4268
|
logger.warn(`notifications: failed to start server: ${String(e)}`);
|
|
4121
4269
|
const result = failLifecycleStartup("failed", e);
|
|
4122
4270
|
finishStartup(result);
|
|
4271
|
+
let suppressExtensionError = false;
|
|
4123
4272
|
let stopped = false;
|
|
4124
4273
|
try {
|
|
4125
4274
|
stopped = await stopSession(id, "session", runtime);
|
|
@@ -4129,9 +4278,10 @@ export function createNotificationsExtension(
|
|
|
4129
4278
|
// than letting it escape startSession and surface a red extension error
|
|
4130
4279
|
// through session_start / session_switch / session_branch.
|
|
4131
4280
|
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
4281
|
+
suppressExtensionError = true;
|
|
4132
4282
|
}
|
|
4133
4283
|
if (!stopped) await cleanupAbandonedStartup();
|
|
4134
|
-
return { ...result, runtime };
|
|
4284
|
+
return { ...result, runtime, suppressExtensionError };
|
|
4135
4285
|
}
|
|
4136
4286
|
}
|
|
4137
4287
|
|
|
@@ -4145,25 +4295,51 @@ export function createNotificationsExtension(
|
|
|
4145
4295
|
if (!runtime || sessionId(binding.context) !== binding.sessionId || activeRuntimeId !== binding.sessionId) {
|
|
4146
4296
|
return "failed";
|
|
4147
4297
|
}
|
|
4148
|
-
runtime.enableNotifications();
|
|
4149
4298
|
return "started";
|
|
4150
4299
|
}
|
|
4151
4300
|
const runtime = runtimes.get(binding.sessionId);
|
|
4152
4301
|
if (runtime) {
|
|
4153
|
-
runtime.enableNotifications();
|
|
4154
4302
|
return "started";
|
|
4155
4303
|
}
|
|
4156
4304
|
const result = await startSession(binding.context);
|
|
4157
4305
|
return result.status === "started" || result.status === "already" ? "started" : result.status;
|
|
4158
4306
|
},
|
|
4159
4307
|
stop: async binding => await stopSession(binding.sessionId, "notifications"),
|
|
4308
|
+
refreshPolicy: (binding, policy) => {
|
|
4309
|
+
const runtime = runtimes.get(binding.sessionId);
|
|
4310
|
+
if (!runtime) return;
|
|
4311
|
+
if (policy.mode === "provisional") {
|
|
4312
|
+
runtime.policyGeneration++;
|
|
4313
|
+
runtime.policySuspended = true;
|
|
4314
|
+
runtime.redact = true;
|
|
4315
|
+
runtime.verbosity = "lean";
|
|
4316
|
+
runtime.stream = false;
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
const redactionEnabled = policy.redact && !runtime.committedRedact;
|
|
4320
|
+
runtime.policyGeneration++;
|
|
4321
|
+
runtime.committedRedact = policy.redact;
|
|
4322
|
+
runtime.policySuspended = false;
|
|
4323
|
+
runtime.redact = policy.redact;
|
|
4324
|
+
runtime.verbosity = policy.verbosity;
|
|
4325
|
+
runtime.stream = policy.stream;
|
|
4326
|
+
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "unknown");
|
|
4327
|
+
},
|
|
4328
|
+
activate: binding => {
|
|
4329
|
+
const runtime = runtimes.get(binding.sessionId);
|
|
4330
|
+
if (!runtime || runtime.stopping) return;
|
|
4331
|
+
runtime.enableNotifications();
|
|
4332
|
+
flushPendingFinal(runtime, runtime.id);
|
|
4333
|
+
for (const present of runtime.deferredGatePresentations.splice(0)) present();
|
|
4334
|
+
for (const processControl of runtime.deferredInboundControls.splice(0)) processControl();
|
|
4335
|
+
},
|
|
4160
4336
|
ensureTelegramDaemon: async binding => {
|
|
4161
4337
|
const { settings, settingsAvailable } = resolveSettings(options.settings);
|
|
4162
4338
|
if (!settingsAvailable || !settings) return "blocked_identity";
|
|
4163
4339
|
try {
|
|
4164
4340
|
return await ensureTelegramOwner(settings, binding.cwd, binding.sessionId);
|
|
4165
4341
|
} catch {
|
|
4166
|
-
return "
|
|
4342
|
+
return "failed";
|
|
4167
4343
|
}
|
|
4168
4344
|
},
|
|
4169
4345
|
};
|
|
@@ -4214,16 +4390,17 @@ export function createNotificationsExtension(
|
|
|
4214
4390
|
const result = await controller.setLocalEnabled(ctx, true);
|
|
4215
4391
|
const enabled = result.status.running && result.status.effectiveEnabled;
|
|
4216
4392
|
const rotated = sessionId(ctx) !== id;
|
|
4393
|
+
if (rotated) await stopSession(id);
|
|
4217
4394
|
const failed = result.outcome === "failed" || (!enabled && !rotated && activeRuntimeId !== id);
|
|
4218
4395
|
ctx.ui.notify(
|
|
4219
|
-
|
|
4220
|
-
? "Notifications enabled
|
|
4221
|
-
:
|
|
4222
|
-
? "Notifications
|
|
4396
|
+
rotated
|
|
4397
|
+
? "Notifications were not enabled because the active session changed during startup."
|
|
4398
|
+
: enabled
|
|
4399
|
+
? "Notifications enabled for this session."
|
|
4223
4400
|
: failed
|
|
4224
4401
|
? "Notifications failed to start for this session."
|
|
4225
4402
|
: "Notifications were not enabled because daemon ownership could not be proved.",
|
|
4226
|
-
|
|
4403
|
+
rotated ? "warning" : enabled ? "info" : failed ? "error" : "warning",
|
|
4227
4404
|
);
|
|
4228
4405
|
return;
|
|
4229
4406
|
}
|
|
@@ -4242,22 +4419,93 @@ export function createNotificationsExtension(
|
|
|
4242
4419
|
},
|
|
4243
4420
|
});
|
|
4244
4421
|
|
|
4422
|
+
const startAndReconcileSession = async (ctx: ExtensionContext): Promise<void> => {
|
|
4423
|
+
const result = await startSession(ctx);
|
|
4424
|
+
if (result.status === "started" || result.status === "already") {
|
|
4425
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4426
|
+
return;
|
|
4427
|
+
}
|
|
4428
|
+
if (
|
|
4429
|
+
!lifecycleStartupCapability &&
|
|
4430
|
+
result.status === "failed" &&
|
|
4431
|
+
!extensionShuttingDown &&
|
|
4432
|
+
!result.suppressExtensionError
|
|
4433
|
+
)
|
|
4434
|
+
throw new Error(`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`);
|
|
4435
|
+
};
|
|
4436
|
+
|
|
4245
4437
|
api.on("session_start", async (_event, ctx) => {
|
|
4246
|
-
await
|
|
4247
|
-
await controller.reconcileCurrentSession(ctx);
|
|
4438
|
+
await startAndReconcileSession(ctx);
|
|
4248
4439
|
});
|
|
4249
4440
|
|
|
4250
4441
|
// A session endpoint's token and generation are authority for exactly one
|
|
4251
4442
|
// session id. `/new`, fork, and resume must all tear down A before publishing
|
|
4252
4443
|
// B. Chat implementations may preserve a topic as metadata, but it must never
|
|
4253
4444
|
// preserve A's endpoint or credentials as B's control/viewing authority.
|
|
4445
|
+
const reconcileBackgroundStartup = (
|
|
4446
|
+
id: string,
|
|
4447
|
+
ctx: ExtensionContext,
|
|
4448
|
+
startup: Promise<SessionStartResult>,
|
|
4449
|
+
): Promise<void> =>
|
|
4450
|
+
startup
|
|
4451
|
+
.then(async result => {
|
|
4452
|
+
if (
|
|
4453
|
+
result.status !== "started" ||
|
|
4454
|
+
extensionShuttingDown ||
|
|
4455
|
+
sessionId(ctx) !== id ||
|
|
4456
|
+
activeRuntimeId !== id ||
|
|
4457
|
+
!runtimes.has(id)
|
|
4458
|
+
)
|
|
4459
|
+
return;
|
|
4460
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4461
|
+
})
|
|
4462
|
+
.catch(error => logger.warn(`notifications: deferred startup reconciliation failed: ${String(error)}`));
|
|
4463
|
+
|
|
4464
|
+
const trackBranchStartup = (id: string, ctx: ExtensionContext, startup: Promise<SessionStartResult>): void => {
|
|
4465
|
+
let status: SessionStartResult["status"] = "failed";
|
|
4466
|
+
void startup.then(
|
|
4467
|
+
result => {
|
|
4468
|
+
status = result.status;
|
|
4469
|
+
},
|
|
4470
|
+
() => {},
|
|
4471
|
+
);
|
|
4472
|
+
const task = reconcileBackgroundStartup(id, ctx, startup);
|
|
4473
|
+
branchStartupTasks.add(task);
|
|
4474
|
+
void task.finally(() => {
|
|
4475
|
+
branchStartupTasks.delete(task);
|
|
4476
|
+
try {
|
|
4477
|
+
options.onBranchStartupSettled?.({ sessionId: id, status });
|
|
4478
|
+
} catch (error) {
|
|
4479
|
+
logger.warn(`notifications: branch startup receipt failed: ${String(error)}`);
|
|
4480
|
+
}
|
|
4481
|
+
});
|
|
4482
|
+
};
|
|
4483
|
+
|
|
4254
4484
|
const rotateSessionAuthority = async (
|
|
4255
4485
|
event: { previousSessionFile?: string },
|
|
4256
4486
|
ctx: ExtensionContext,
|
|
4487
|
+
awaitStartup: boolean,
|
|
4257
4488
|
): Promise<void> => {
|
|
4258
4489
|
if (extensionShuttingDown) return;
|
|
4259
4490
|
const newId = sessionId(ctx);
|
|
4260
4491
|
const prevId = activeRuntimeId ?? sessionIdFromFile(event.previousSessionFile);
|
|
4492
|
+
if (prevId === newId) {
|
|
4493
|
+
const pendingStartup = sessionStartPromises.get(newId);
|
|
4494
|
+
if (pendingStartup) {
|
|
4495
|
+
if (awaitStartup) {
|
|
4496
|
+
await pendingStartup;
|
|
4497
|
+
if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
|
|
4498
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4499
|
+
} else {
|
|
4500
|
+
trackBranchStartup(newId, ctx, pendingStartup);
|
|
4501
|
+
}
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
if (runtimes.has(newId)) {
|
|
4505
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4261
4509
|
if (prevId && prevId !== newId) {
|
|
4262
4510
|
controller.rekeySession(prevId, newId);
|
|
4263
4511
|
try {
|
|
@@ -4271,26 +4519,35 @@ export function createNotificationsExtension(
|
|
|
4271
4519
|
}
|
|
4272
4520
|
}
|
|
4273
4521
|
if (extensionShuttingDown) return;
|
|
4274
|
-
|
|
4275
|
-
if (
|
|
4276
|
-
await
|
|
4522
|
+
const startup = startSession(ctx);
|
|
4523
|
+
if (awaitStartup) {
|
|
4524
|
+
const result = await startup;
|
|
4525
|
+
if (!lifecycleStartupCapability && result.status === "failed")
|
|
4526
|
+
throw new Error(
|
|
4527
|
+
`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`,
|
|
4528
|
+
);
|
|
4529
|
+
if (extensionShuttingDown) {
|
|
4530
|
+
await stopSession(newId);
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
await controller.reconcileCurrentSession(ctx);
|
|
4277
4534
|
return;
|
|
4278
4535
|
}
|
|
4279
|
-
|
|
4536
|
+
trackBranchStartup(newId, ctx, startup);
|
|
4280
4537
|
};
|
|
4281
4538
|
api.on("session_switch", async (event, ctx) => {
|
|
4282
4539
|
if (identityControlInFlight) {
|
|
4283
|
-
deferredIdentityRotation = { event, ctx };
|
|
4540
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: true };
|
|
4284
4541
|
return;
|
|
4285
4542
|
}
|
|
4286
|
-
await rotateSessionAuthority(event, ctx);
|
|
4543
|
+
await rotateSessionAuthority(event, ctx, true);
|
|
4287
4544
|
});
|
|
4288
4545
|
api.on("session_branch", async (event, ctx) => {
|
|
4289
4546
|
if (identityControlInFlight) {
|
|
4290
|
-
deferredIdentityRotation = { event, ctx };
|
|
4547
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: false };
|
|
4291
4548
|
return;
|
|
4292
4549
|
}
|
|
4293
|
-
await rotateSessionAuthority(event, ctx);
|
|
4550
|
+
await rotateSessionAuthority(event, ctx, false);
|
|
4294
4551
|
});
|
|
4295
4552
|
|
|
4296
4553
|
const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
|
|
@@ -4306,6 +4563,36 @@ export function createNotificationsExtension(
|
|
|
4306
4563
|
rt.inFlightTools.clear();
|
|
4307
4564
|
};
|
|
4308
4565
|
|
|
4566
|
+
const resetTurnStreamState = (rt: SessionRuntime): void => {
|
|
4567
|
+
rt.currentTurnText = undefined;
|
|
4568
|
+
rt.preAskFlushedText = undefined;
|
|
4569
|
+
rt.liveRef = undefined;
|
|
4570
|
+
rt.turnClosed = true;
|
|
4571
|
+
rt.lastLiveAt = undefined;
|
|
4572
|
+
rt.lastLiveText = undefined;
|
|
4573
|
+
};
|
|
4574
|
+
|
|
4575
|
+
const flushPendingFinal = (rt: SessionRuntime, id: string): void => {
|
|
4576
|
+
const pending = rt.pendingFinal;
|
|
4577
|
+
if (!pending) return;
|
|
4578
|
+
rt.pendingFinal = undefined;
|
|
4579
|
+
if (pending.text && rt.notificationsActive && !rt.redact) {
|
|
4580
|
+
try {
|
|
4581
|
+
pushSessionFrame(rt, {
|
|
4582
|
+
type: "turn_stream",
|
|
4583
|
+
sessionId: id,
|
|
4584
|
+
phase: "finalized",
|
|
4585
|
+
finalAnswer: true,
|
|
4586
|
+
text: pending.text,
|
|
4587
|
+
...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
|
|
4588
|
+
});
|
|
4589
|
+
} catch (error) {
|
|
4590
|
+
logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
resetTurnStreamState(rt);
|
|
4594
|
+
};
|
|
4595
|
+
|
|
4309
4596
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
4310
4597
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
4311
4598
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -4417,7 +4704,9 @@ export function createNotificationsExtension(
|
|
|
4417
4704
|
const model = (ctx as { getModel?: () => { id?: string } | undefined }).getModel?.();
|
|
4418
4705
|
const tokenUsage = usage && usage.tokens != null ? `${usage.tokens}/${usage.contextWindow}` : undefined;
|
|
4419
4706
|
const modelId = model?.id;
|
|
4420
|
-
|
|
4707
|
+
const generation = rt.policyGeneration;
|
|
4708
|
+
void (options.readNotificationDiffStat ?? readGitDiffStat)(ctx.cwd).then(diff => {
|
|
4709
|
+
if (!canDeliverAsync(rt, generation)) return;
|
|
4421
4710
|
const cwd = compactCwd(ctx.cwd);
|
|
4422
4711
|
if (!diff && !tokenUsage && !modelId && !cwd) return;
|
|
4423
4712
|
try {
|
|
@@ -4445,7 +4734,7 @@ export function createNotificationsExtension(
|
|
|
4445
4734
|
// Push the in-flight turn's assistant text as a finalized turn_stream, deduped
|
|
4446
4735
|
// against what was already flushed for this turn (the pre-ask lead-in).
|
|
4447
4736
|
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
|
|
4448
|
-
if (!text || text === rt.preAskFlushedText || !rt.notificationsActive) return;
|
|
4737
|
+
if (!text || text === rt.preAskFlushedText || !rt.notificationsActive || rt.policySuspended) return;
|
|
4449
4738
|
rt.preAskFlushedText = text;
|
|
4450
4739
|
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
4451
4740
|
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
@@ -4575,18 +4864,20 @@ export function createNotificationsExtension(
|
|
|
4575
4864
|
const id = sessionId(ctx);
|
|
4576
4865
|
const rt = runtimes.get(id);
|
|
4577
4866
|
if (!rt?.notificationsActive) return;
|
|
4578
|
-
const text = rt.
|
|
4867
|
+
const text = rt.policySuspended
|
|
4868
|
+
? rt.committedRedact
|
|
4869
|
+
? undefined
|
|
4870
|
+
: summaryFromMessage(event.message, turnTextMax())
|
|
4871
|
+
: rt.redact
|
|
4872
|
+
? undefined
|
|
4873
|
+
: summaryFromMessage(event.message, turnTextMax());
|
|
4874
|
+
if (rt.policySuspended) {
|
|
4875
|
+
rt.pendingFinal = { text, messageRef: rt.liveRef };
|
|
4876
|
+
rt.turnClosed = true;
|
|
4877
|
+
return;
|
|
4878
|
+
}
|
|
4579
4879
|
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;
|
|
4880
|
+
resetTurnStreamState(rt);
|
|
4590
4881
|
});
|
|
4591
4882
|
|
|
4592
4883
|
// Live streaming (opt-in): push throttled in-progress assistant text as
|
|
@@ -4596,6 +4887,7 @@ export function createNotificationsExtension(
|
|
|
4596
4887
|
api.on("message_update", (event, ctx) => {
|
|
4597
4888
|
const id = sessionId(ctx);
|
|
4598
4889
|
const rt = runtimes.get(id);
|
|
4890
|
+
rt?.emitPromptEvent(event);
|
|
4599
4891
|
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
|
|
4600
4892
|
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
4601
4893
|
if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
@@ -4619,6 +4911,7 @@ export function createNotificationsExtension(
|
|
|
4619
4911
|
api.on("message_end", (event, ctx) => {
|
|
4620
4912
|
const id = sessionId(ctx);
|
|
4621
4913
|
const rt = runtimes.get(id);
|
|
4914
|
+
rt?.emitPromptEvent(event);
|
|
4622
4915
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4623
4916
|
// Capture the in-flight ASSISTANT text here (message_end is on the awaited
|
|
4624
4917
|
// extension path and ordered before tool_execution_start) so the pre-ask
|
|
@@ -4647,6 +4940,7 @@ export function createNotificationsExtension(
|
|
|
4647
4940
|
extensionShuttingDown = true;
|
|
4648
4941
|
identityControlInFlight = false;
|
|
4649
4942
|
deferredIdentityRotation = undefined;
|
|
4943
|
+
await Promise.allSettled([...branchStartupTasks]);
|
|
4650
4944
|
const id = sessionId(ctx);
|
|
4651
4945
|
const rt = runtimes.get(id);
|
|
4652
4946
|
if (rt) terminalizeInFlightTools(rt, id, "unknown");
|