@gajae-code/coding-agent 0.11.11 → 0.12.0
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 +49 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +22 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +8 -0
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +15 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +44 -10
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +25 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +1 -1
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +23 -1
- package/src/config/settings.ts +246 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/types.ts +28 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +742 -85
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +8 -9
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +43 -7
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +19 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +19 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +341 -38
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +13 -1
- package/src/session/agent-session.ts +466 -50
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +764 -17
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
package/src/sdk/bus/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../
|
|
|
35
35
|
import { isAuthenticated, kNoAuth } from "../../config/model-registry";
|
|
36
36
|
import { Settings } from "../../config/settings";
|
|
37
37
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
38
|
+
import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
|
|
38
39
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
39
40
|
import {
|
|
40
41
|
NotificationGatePolicyChangedError,
|
|
@@ -82,9 +83,16 @@ import {
|
|
|
82
83
|
sessionTag,
|
|
83
84
|
} from "./config";
|
|
84
85
|
import { telegramControlCommandUsage } from "./config-commands";
|
|
86
|
+
import {
|
|
87
|
+
isNativeControlDrainAvailable,
|
|
88
|
+
runIdentityControlSuccessPath,
|
|
89
|
+
type TerminalSendOutcome,
|
|
90
|
+
} from "./control-drain-lease";
|
|
85
91
|
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
|
|
92
|
+
import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
|
|
86
93
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
87
94
|
import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
|
|
95
|
+
import { createReconciliationStore } from "./reconciliation-store";
|
|
88
96
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
89
97
|
import {
|
|
90
98
|
ASK_SELECTED_ACK_CAPABILITY,
|
|
@@ -95,6 +103,17 @@ import {
|
|
|
95
103
|
unregisterNotificationRoot,
|
|
96
104
|
} from "./telegram-daemon";
|
|
97
105
|
|
|
106
|
+
export type {
|
|
107
|
+
IdentityControlSuccessPathInput,
|
|
108
|
+
IdentityControlTerminalPathInput,
|
|
109
|
+
TerminalSendOutcome,
|
|
110
|
+
} from "./control-drain-lease";
|
|
111
|
+
export {
|
|
112
|
+
isNativeControlDrainAvailable,
|
|
113
|
+
runIdentityControlSuccessPath,
|
|
114
|
+
runIdentityControlTerminalPath,
|
|
115
|
+
} from "./control-drain-lease";
|
|
116
|
+
|
|
98
117
|
// ===========================================================================
|
|
99
118
|
// Session lifecycle control protocol (TypeScript mirror of the Rust wire
|
|
100
119
|
// contract in `crates/gjc-sdk/src/lifecycle.rs`).
|
|
@@ -886,6 +905,8 @@ interface SessionRuntime {
|
|
|
886
905
|
disposeGateListener: () => void;
|
|
887
906
|
/** Whether notification-only delivery and answer resources are active. */
|
|
888
907
|
notificationsActive: boolean;
|
|
908
|
+
/** Rejects new SDK frames while a leased terminal response drains. */
|
|
909
|
+
inboundFenced: boolean;
|
|
889
910
|
/** Set as soon as terminal teardown is requested, before startup settles. */
|
|
890
911
|
stopping: boolean;
|
|
891
912
|
/** Recreates notification-only resources after `/notify on`. */
|
|
@@ -1807,6 +1828,9 @@ function sdkQuerySurface(
|
|
|
1807
1828
|
}),
|
|
1808
1829
|
configOverrides: ReadonlyMap<string, unknown> = new Map(),
|
|
1809
1830
|
promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
|
|
1831
|
+
skillStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
|
|
1832
|
+
status: "unknown",
|
|
1833
|
+
}),
|
|
1810
1834
|
): SessionSurface {
|
|
1811
1835
|
const metadata = () => ({
|
|
1812
1836
|
sessionId: id,
|
|
@@ -1935,6 +1959,8 @@ function sdkQuerySurface(
|
|
|
1935
1959
|
getJobs: () => ctx.getJobs(),
|
|
1936
1960
|
getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1937
1961
|
promptStatusLookup(selector),
|
|
1962
|
+
getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1963
|
+
skillStatusLookup(selector),
|
|
1938
1964
|
installedQueries: installedOperations(ctx, "query"),
|
|
1939
1965
|
};
|
|
1940
1966
|
}
|
|
@@ -1962,7 +1988,7 @@ function sdkControlSurface(
|
|
|
1962
1988
|
requesterConnectionId?: string,
|
|
1963
1989
|
clientRef?: string,
|
|
1964
1990
|
trackReconciliation?: boolean,
|
|
1965
|
-
) => void = () => {},
|
|
1991
|
+
) => void | Promise<void> = () => {},
|
|
1966
1992
|
onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
|
|
1967
1993
|
acceptGateResolution: () => boolean,
|
|
1968
1994
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
|
|
@@ -1971,6 +1997,20 @@ function sdkControlSurface(
|
|
|
1971
1997
|
settings?: Settings,
|
|
1972
1998
|
configOverrides: Map<string, unknown> = new Map(),
|
|
1973
1999
|
configRevision: { current: number } = { current: 0 },
|
|
2000
|
+
skillRecon?: {
|
|
2001
|
+
admit: (clientRef?: string) => void;
|
|
2002
|
+
release: (clientRef?: string) => void;
|
|
2003
|
+
noteAccepted: (
|
|
2004
|
+
correlation: { commandId: string; turnId: string },
|
|
2005
|
+
clientRef?: string,
|
|
2006
|
+
extra?: { skillName?: string },
|
|
2007
|
+
) => Promise<void>;
|
|
2008
|
+
noteTransition: (
|
|
2009
|
+
correlation: { commandId: string; turnId: string } | undefined,
|
|
2010
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
2011
|
+
) => Promise<void>;
|
|
2012
|
+
lookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
2013
|
+
},
|
|
1974
2014
|
): ControlSurface {
|
|
1975
2015
|
const unavailable = (operation: string, reason: string) => () => {
|
|
1976
2016
|
throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
|
|
@@ -2101,12 +2141,17 @@ function sdkControlSurface(
|
|
|
2101
2141
|
error: Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" }),
|
|
2102
2142
|
});
|
|
2103
2143
|
pendingPreflightCancellations.add(cancelPreflight);
|
|
2104
|
-
const
|
|
2144
|
+
const settleAccepted = async () => {
|
|
2105
2145
|
if (preflightSettled) return;
|
|
2106
2146
|
accepted = true;
|
|
2107
|
-
onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
|
|
2147
|
+
await onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
|
|
2108
2148
|
settlePreflight({ status: "accepted" });
|
|
2109
2149
|
};
|
|
2150
|
+
// Durable fence preferred; keep legacy onPreflightAccepted for hosts/tests that only fire the sync hook.
|
|
2151
|
+
const onPreflightAcceptCommit = settleAccepted;
|
|
2152
|
+
const onPreflightAccepted = () => {
|
|
2153
|
+
void settleAccepted();
|
|
2154
|
+
};
|
|
2110
2155
|
// Do not acknowledge the prompt until AgentSession's async preflight
|
|
2111
2156
|
// succeeds. The terminal result records correlation before agent_start can fire.
|
|
2112
2157
|
let submission: Promise<void> | undefined;
|
|
@@ -2114,6 +2159,7 @@ function sdkControlSurface(
|
|
|
2114
2159
|
submission = Promise.resolve(
|
|
2115
2160
|
api.sendUserMessage(content, {
|
|
2116
2161
|
...(deliverAs ? { deliverAs } : !forceFresh && isBusy() ? { deliverAs: "steer" as const } : {}),
|
|
2162
|
+
onPreflightAcceptCommit,
|
|
2117
2163
|
onPreflightAccepted,
|
|
2118
2164
|
}),
|
|
2119
2165
|
);
|
|
@@ -2324,13 +2370,84 @@ function sdkControlSurface(
|
|
|
2324
2370
|
code: "terminal_uncertain",
|
|
2325
2371
|
});
|
|
2326
2372
|
},
|
|
2327
|
-
invokeSkill: (name, args) => {
|
|
2373
|
+
invokeSkill: async (name, args, clientRef) => {
|
|
2328
2374
|
if (!bindings.has("invokeSkill") || !ctx.invokeSkill)
|
|
2329
2375
|
return unavailable("skill.invoke", "no skill invocation seam is installed")();
|
|
2330
2376
|
|
|
2331
2377
|
if (typeof args !== "undefined" && typeof args !== "string")
|
|
2332
2378
|
throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
|
|
2333
|
-
|
|
2379
|
+
const trimmedClientRef =
|
|
2380
|
+
typeof clientRef === "string" ? clientRef.trim() : clientRef === undefined ? undefined : "";
|
|
2381
|
+
if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > 128))
|
|
2382
|
+
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2383
|
+
code: "invalid_input",
|
|
2384
|
+
});
|
|
2385
|
+
const commandId = crypto.randomUUID();
|
|
2386
|
+
const turnId = crypto.randomUUID();
|
|
2387
|
+
const correlation = { commandId, turnId };
|
|
2388
|
+
if (skillRecon) skillRecon.admit(trimmedClientRef);
|
|
2389
|
+
const { promise: acceptedP, resolve, reject } = Promise.withResolvers<Record<string, unknown>>();
|
|
2390
|
+
let phase: "pending" | "accepted" | "rejected" = "pending";
|
|
2391
|
+
const settleAccept = (value: Record<string, unknown>) => {
|
|
2392
|
+
if (phase !== "pending") return;
|
|
2393
|
+
phase = "accepted";
|
|
2394
|
+
resolve(value);
|
|
2395
|
+
};
|
|
2396
|
+
const settleReject = (error: unknown) => {
|
|
2397
|
+
if (phase !== "pending") return;
|
|
2398
|
+
phase = "rejected";
|
|
2399
|
+
reject(error);
|
|
2400
|
+
};
|
|
2401
|
+
let prepared: { name: string; path: string; lineCount?: number; cleanedArgs?: string } | undefined;
|
|
2402
|
+
const run = Promise.resolve(
|
|
2403
|
+
ctx.invokeSkill(name, args as string | undefined, {
|
|
2404
|
+
onSkillPrepared: meta => {
|
|
2405
|
+
prepared = meta;
|
|
2406
|
+
},
|
|
2407
|
+
onPreflightAcceptCommit: async () => {
|
|
2408
|
+
const meta = prepared ?? { name: String(name), path: "" };
|
|
2409
|
+
if (skillRecon)
|
|
2410
|
+
await skillRecon.noteAccepted(correlation, trimmedClientRef, { skillName: meta.name });
|
|
2411
|
+
settleAccept({
|
|
2412
|
+
accepted: true,
|
|
2413
|
+
commandId,
|
|
2414
|
+
turnId,
|
|
2415
|
+
name: meta.name,
|
|
2416
|
+
path: meta.path,
|
|
2417
|
+
...(meta.lineCount !== undefined ? { lineCount: meta.lineCount } : {}),
|
|
2418
|
+
...(meta.cleanedArgs !== undefined ? { args: meta.cleanedArgs } : {}),
|
|
2419
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2420
|
+
});
|
|
2421
|
+
},
|
|
2422
|
+
}),
|
|
2423
|
+
).then(
|
|
2424
|
+
result => {
|
|
2425
|
+
if (phase === "pending") {
|
|
2426
|
+
// Completed without fence (e.g. queue path) — still surface result.
|
|
2427
|
+
settleAccept({
|
|
2428
|
+
accepted: true,
|
|
2429
|
+
commandId,
|
|
2430
|
+
turnId,
|
|
2431
|
+
...(typeof result === "object" && result ? (result as object) : {}),
|
|
2432
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2433
|
+
});
|
|
2434
|
+
} else if (skillRecon) {
|
|
2435
|
+
void skillRecon.noteTransition(correlation, { type: "agent_end" });
|
|
2436
|
+
}
|
|
2437
|
+
return result;
|
|
2438
|
+
},
|
|
2439
|
+
error => {
|
|
2440
|
+
if (phase === "pending") {
|
|
2441
|
+
if (skillRecon) skillRecon.release(trimmedClientRef);
|
|
2442
|
+
settleReject(error);
|
|
2443
|
+
} else if (skillRecon) {
|
|
2444
|
+
void skillRecon.noteTransition(correlation, { type: "agent_failed", error });
|
|
2445
|
+
}
|
|
2446
|
+
throw error;
|
|
2447
|
+
},
|
|
2448
|
+
);
|
|
2449
|
+
void run.catch(() => {});
|
|
2450
|
+
return await acceptedP;
|
|
2334
2451
|
},
|
|
2335
2452
|
setPlanMode: async on => {
|
|
2336
2453
|
if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
|
|
@@ -2866,6 +2983,18 @@ export async function ensureConfiguredProviderDaemons(
|
|
|
2866
2983
|
if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
|
|
2867
2984
|
}
|
|
2868
2985
|
|
|
2986
|
+
/**
|
|
2987
|
+
* Classify whether a session identity event must await notification endpoint startup.
|
|
2988
|
+
* Only an interactive selector resume may defer this ancillary startup; all other
|
|
2989
|
+
* events, including branches and unknown origins, fail closed to awaiting it.
|
|
2990
|
+
*/
|
|
2991
|
+
export function shouldAwaitNotificationStartup(event: {
|
|
2992
|
+
type: "session_switch" | "session_branch";
|
|
2993
|
+
transition?: { origin: string };
|
|
2994
|
+
}): boolean {
|
|
2995
|
+
return event.type !== "session_switch" || event.transition?.origin !== INTERACTIVE_SELECTOR_RESUME_ORIGIN;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2869
2998
|
export function createNotificationsExtension(
|
|
2870
2999
|
api: ExtensionAPI,
|
|
2871
3000
|
options: {
|
|
@@ -2905,10 +3034,15 @@ export function createNotificationsExtension(
|
|
|
2905
3034
|
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2906
3035
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2907
3036
|
const branchStartupTasks = new Set<Promise<void>>();
|
|
3037
|
+
const sessionLifecycleTasks = new Set<Promise<void>>();
|
|
2908
3038
|
let activeRuntimeId: string | undefined;
|
|
2909
3039
|
let identityControlInFlight = false;
|
|
2910
3040
|
let deferredIdentityRotation:
|
|
2911
|
-
| {
|
|
3041
|
+
| {
|
|
3042
|
+
event: { previousSessionFile?: string; transition?: { origin: string } };
|
|
3043
|
+
ctx: ExtensionContext;
|
|
3044
|
+
awaitStartup: boolean;
|
|
3045
|
+
}
|
|
2912
3046
|
| undefined;
|
|
2913
3047
|
let extensionShuttingDown = false;
|
|
2914
3048
|
|
|
@@ -2969,6 +3103,7 @@ export function createNotificationsExtension(
|
|
|
2969
3103
|
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2970
3104
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2971
3105
|
if (reason === "session" && requestedRuntime) {
|
|
3106
|
+
requestedRuntime.inboundFenced = true;
|
|
2972
3107
|
requestedRuntime.stopping = true;
|
|
2973
3108
|
requestedRuntime.abortEphemeralTurns();
|
|
2974
3109
|
}
|
|
@@ -3288,10 +3423,29 @@ export function createNotificationsExtension(
|
|
|
3288
3423
|
// (contract documented in ./prompt-reconciliation and ../prompt-status).
|
|
3289
3424
|
// Active records never age into terminal; documented TTL/capacity
|
|
3290
3425
|
// eviction is the only removal, after which lookups report `unknown`.
|
|
3426
|
+
const sessionFile =
|
|
3427
|
+
typeof ctx.sessionManager?.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : null;
|
|
3428
|
+
const reconciliationSessionId =
|
|
3429
|
+
typeof ctx.sessionManager?.getSessionId === "function" ? ctx.sessionManager.getSessionId() : "";
|
|
3430
|
+
const durableStore =
|
|
3431
|
+
sessionFile && reconciliationSessionId
|
|
3432
|
+
? createReconciliationStore({ sessionFile, sessionId: String(reconciliationSessionId) })
|
|
3433
|
+
: null;
|
|
3434
|
+
const kindReconciliation = createKindAwareReconciliation({ store: durableStore });
|
|
3435
|
+
if (durableStore) void kindReconciliation.hydrateFromStore();
|
|
3436
|
+
// Backward-compatible process-local prompt reconciler kept for unit-test isolation;
|
|
3437
|
+
// production path uses kindReconciliation for prompt+skill.
|
|
3291
3438
|
const reconciliation = createPromptReconciliation();
|
|
3292
|
-
const admitPromptSubmission =
|
|
3293
|
-
const notePromptReconciliationAccepted =
|
|
3294
|
-
|
|
3439
|
+
const admitPromptSubmission = (clientRef?: string) => kindReconciliation.admit("prompt", clientRef);
|
|
3440
|
+
const notePromptReconciliationAccepted = async (
|
|
3441
|
+
correlation: { commandId: string; turnId: string },
|
|
3442
|
+
clientRef?: string,
|
|
3443
|
+
) => {
|
|
3444
|
+
await kindReconciliation.noteAccepted("prompt", correlation, clientRef);
|
|
3445
|
+
};
|
|
3446
|
+
const lookupPromptStatus = (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
3447
|
+
kindReconciliation.lookup("prompt", selector);
|
|
3448
|
+
const releasePromptAdmission = (clientRef?: string) => kindReconciliation.releaseAdmission("prompt", clientRef);
|
|
3295
3449
|
const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
|
|
3296
3450
|
const pendingIndex = pendingPromptCorrelations.findIndex(
|
|
3297
3451
|
candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
|
|
@@ -3318,6 +3472,7 @@ export function createNotificationsExtension(
|
|
|
3318
3472
|
addTerminalTombstone(key);
|
|
3319
3473
|
};
|
|
3320
3474
|
const cleanupPromptRecords = (now = Date.now()) => {
|
|
3475
|
+
kindReconciliation.cleanup();
|
|
3321
3476
|
reconciliation.cleanup();
|
|
3322
3477
|
for (const [key, expiresAt] of promptTerminalTombstones)
|
|
3323
3478
|
if (expiresAt <= now) promptTerminalTombstones.delete(key);
|
|
@@ -3396,7 +3551,7 @@ export function createNotificationsExtension(
|
|
|
3396
3551
|
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3397
3552
|
}
|
|
3398
3553
|
};
|
|
3399
|
-
const recordPromptAccepted = (
|
|
3554
|
+
const recordPromptAccepted = async (
|
|
3400
3555
|
correlation: { commandId: string; turnId: string },
|
|
3401
3556
|
requesterConnectionId?: string,
|
|
3402
3557
|
clientRef?: string,
|
|
@@ -3405,10 +3560,11 @@ export function createNotificationsExtension(
|
|
|
3405
3560
|
if (!requesterConnectionId) {
|
|
3406
3561
|
// No delivery owner: tracked prompts cannot be reconciled. Release
|
|
3407
3562
|
// their admission reservation instead of leaking the active slot.
|
|
3408
|
-
if (trackReconciliation)
|
|
3563
|
+
if (trackReconciliation) releasePromptAdmission(clientRef);
|
|
3409
3564
|
return;
|
|
3410
3565
|
}
|
|
3411
|
-
|
|
3566
|
+
// Register delivery ownership synchronously before any await so post-accept
|
|
3567
|
+
// failures that race the durable write still emit correlated terminals.
|
|
3412
3568
|
cleanupPromptRecords();
|
|
3413
3569
|
while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
|
|
3414
3570
|
const oldest = promptSubmissions.entries().next().value as [string, PromptSubmission] | undefined;
|
|
@@ -3427,6 +3583,7 @@ export function createNotificationsExtension(
|
|
|
3427
3583
|
createdAt: Date.now(),
|
|
3428
3584
|
bufferedFrames: [],
|
|
3429
3585
|
});
|
|
3586
|
+
if (trackReconciliation) await notePromptReconciliationAccepted(correlation, clientRef);
|
|
3430
3587
|
};
|
|
3431
3588
|
const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
|
|
3432
3589
|
if (!correlation) return false;
|
|
@@ -3440,7 +3597,7 @@ export function createNotificationsExtension(
|
|
|
3440
3597
|
};
|
|
3441
3598
|
const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
3442
3599
|
const sanitized = sanitizePromptFailure(error);
|
|
3443
|
-
|
|
3600
|
+
void kindReconciliation.noteTransition("prompt", correlation, { type: "agent_failed", error: sanitized });
|
|
3444
3601
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3445
3602
|
if (!submission || !runtime || !recordPromptTerminal(correlation)) return;
|
|
3446
3603
|
emitPromptLifecycle(correlation, {
|
|
@@ -3491,6 +3648,7 @@ export function createNotificationsExtension(
|
|
|
3491
3648
|
},
|
|
3492
3649
|
configOverrides,
|
|
3493
3650
|
lookupPromptStatus,
|
|
3651
|
+
selector => kindReconciliation.lookup("skill", selector),
|
|
3494
3652
|
),
|
|
3495
3653
|
id,
|
|
3496
3654
|
revisions,
|
|
@@ -3507,10 +3665,18 @@ export function createNotificationsExtension(
|
|
|
3507
3665
|
() => runtime?.stopping !== true,
|
|
3508
3666
|
trackGateResolution,
|
|
3509
3667
|
admitPromptSubmission,
|
|
3510
|
-
|
|
3668
|
+
releasePromptAdmission,
|
|
3511
3669
|
settings,
|
|
3512
3670
|
configOverrides,
|
|
3513
3671
|
configRevision,
|
|
3672
|
+
{
|
|
3673
|
+
admit: (clientRef?: string) => kindReconciliation.admit("skill", clientRef),
|
|
3674
|
+
release: (clientRef?: string) => kindReconciliation.releaseAdmission("skill", clientRef),
|
|
3675
|
+
noteAccepted: (correlation, clientRef, extra) =>
|
|
3676
|
+
kindReconciliation.noteAccepted("skill", correlation, clientRef, extra),
|
|
3677
|
+
noteTransition: (correlation, frame) => kindReconciliation.noteTransition("skill", correlation, frame),
|
|
3678
|
+
lookup: selector => kindReconciliation.lookup("skill", selector),
|
|
3679
|
+
},
|
|
3514
3680
|
);
|
|
3515
3681
|
const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
|
|
3516
3682
|
if (
|
|
@@ -3576,6 +3742,75 @@ export function createNotificationsExtension(
|
|
|
3576
3742
|
};
|
|
3577
3743
|
},
|
|
3578
3744
|
onRequest: options.onSdkRequest,
|
|
3745
|
+
beforeControlResponse: async (_connectionId, request, response, sendTerminal) => {
|
|
3746
|
+
if (typeof request.operation !== "string" || !identityControlOperations.has(request.operation)) return;
|
|
3747
|
+
const pending = deferredIdentityRotation;
|
|
3748
|
+
deferredIdentityRotation = undefined;
|
|
3749
|
+
identityControlInFlight = false;
|
|
3750
|
+
if (response.ok !== true || !pending) return;
|
|
3751
|
+
|
|
3752
|
+
const predecessorId = activeRuntimeId ?? sessionIdFromFile(pending.event.previousSessionFile);
|
|
3753
|
+
if (!predecessorId) throw new Error("notifications: identity control predecessor is unknown.");
|
|
3754
|
+
let terminalAttempted = false;
|
|
3755
|
+
let terminalOutcome: TerminalSendOutcome | undefined;
|
|
3756
|
+
try {
|
|
3757
|
+
terminalOutcome = await runIdentityControlSuccessPath({
|
|
3758
|
+
fence: () => {
|
|
3759
|
+
const predecessor = runtimes.get(predecessorId);
|
|
3760
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
3761
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} cannot be fenced.`);
|
|
3762
|
+
predecessor.inboundFenced = true;
|
|
3763
|
+
},
|
|
3764
|
+
ensurePredecessorSendCapable: () => {
|
|
3765
|
+
const predecessor = runtimes.get(predecessorId);
|
|
3766
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
3767
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} is not send-capable.`);
|
|
3768
|
+
},
|
|
3769
|
+
startSuccessor: async () => {
|
|
3770
|
+
const successorId = sessionId(pending.ctx);
|
|
3771
|
+
try {
|
|
3772
|
+
await rotateSessionAuthority(pending.event, pending.ctx, true, {
|
|
3773
|
+
deferPredecessorStop: true,
|
|
3774
|
+
});
|
|
3775
|
+
const successor = runtimes.get(successorId);
|
|
3776
|
+
if (!successor?.host.started || activeRuntimeId !== successorId)
|
|
3777
|
+
throw new Error(`notifications: successor runtime ${successorId} was not ready.`);
|
|
3778
|
+
} catch (error) {
|
|
3779
|
+
(response as Record<string, unknown>).ok = false;
|
|
3780
|
+
delete (response as Record<string, unknown>).result;
|
|
3781
|
+
(response as Record<string, unknown>).error = {
|
|
3782
|
+
code: "unavailable",
|
|
3783
|
+
message: error instanceof Error ? error.message : "Successor session was not ready.",
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
},
|
|
3787
|
+
sendTerminal: async () => {
|
|
3788
|
+
terminalAttempted = true;
|
|
3789
|
+
try {
|
|
3790
|
+
await sendTerminal();
|
|
3791
|
+
return "written";
|
|
3792
|
+
} catch {
|
|
3793
|
+
return "write_failed";
|
|
3794
|
+
}
|
|
3795
|
+
},
|
|
3796
|
+
stopPredecessor: async () => {
|
|
3797
|
+
try {
|
|
3798
|
+
await stopSession(predecessorId);
|
|
3799
|
+
} catch (error) {
|
|
3800
|
+
if (!terminalAttempted) throw error;
|
|
3801
|
+
logger.error(`notifications: deferred predecessor cleanup failed: ${String(error)}`);
|
|
3802
|
+
}
|
|
3803
|
+
},
|
|
3804
|
+
requireNativeControlDrain:
|
|
3805
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true,
|
|
3806
|
+
});
|
|
3807
|
+
} catch (error) {
|
|
3808
|
+
if (!terminalAttempted) throw error;
|
|
3809
|
+
logger.error(
|
|
3810
|
+
`notifications: identity terminal delivery failed (${terminalOutcome ?? "unknown"}): ${String(error)}`,
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
},
|
|
3579
3814
|
afterControlResponse: async (connectionId, request, response) => {
|
|
3580
3815
|
if (
|
|
3581
3816
|
(request.operation === "turn.prompt" ||
|
|
@@ -3596,13 +3831,6 @@ export function createNotificationsExtension(
|
|
|
3596
3831
|
}
|
|
3597
3832
|
|
|
3598
3833
|
if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
|
|
3599
|
-
if (typeof request.operation === "string" && identityControlOperations.has(request.operation)) {
|
|
3600
|
-
const pending = deferredIdentityRotation;
|
|
3601
|
-
deferredIdentityRotation = undefined;
|
|
3602
|
-
identityControlInFlight = false;
|
|
3603
|
-
if (response.ok === true && pending)
|
|
3604
|
-
await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
|
|
3605
|
-
}
|
|
3606
3834
|
},
|
|
3607
3835
|
control: async (connectionId, frame) => {
|
|
3608
3836
|
const request = frame as {
|
|
@@ -3622,6 +3850,21 @@ export function createNotificationsExtension(
|
|
|
3622
3850
|
ok: false,
|
|
3623
3851
|
error: { code: "conflict", message: "session identity mutation is already active" },
|
|
3624
3852
|
};
|
|
3853
|
+
const requireNativeControlDrain =
|
|
3854
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true ||
|
|
3855
|
+
(!!request.input &&
|
|
3856
|
+
typeof request.input === "object" &&
|
|
3857
|
+
!Array.isArray(request.input) &&
|
|
3858
|
+
(request.input as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true);
|
|
3859
|
+
if (requireNativeControlDrain && !isNativeControlDrainAvailable())
|
|
3860
|
+
return {
|
|
3861
|
+
id: requestId,
|
|
3862
|
+
ok: false,
|
|
3863
|
+
error: {
|
|
3864
|
+
code: "unavailable",
|
|
3865
|
+
message: "SDK identity control requires the native control-drain lease.",
|
|
3866
|
+
},
|
|
3867
|
+
};
|
|
3625
3868
|
if (rotatesIdentity) identityControlInFlight = true;
|
|
3626
3869
|
const response = await controlRequesterContext.run(connectionId, () =>
|
|
3627
3870
|
dispatchControl(
|
|
@@ -3693,6 +3936,7 @@ export function createNotificationsExtension(
|
|
|
3693
3936
|
workflowGate: undefined,
|
|
3694
3937
|
gatePresentations,
|
|
3695
3938
|
stopping: false,
|
|
3939
|
+
inboundFenced: false,
|
|
3696
3940
|
abortEphemeralTurns: () => {},
|
|
3697
3941
|
disableEphemeralTurns: () => {},
|
|
3698
3942
|
cancelPostmortemCleanup: () => {},
|
|
@@ -3708,7 +3952,9 @@ export function createNotificationsExtension(
|
|
|
3708
3952
|
pendingPromptCorrelations,
|
|
3709
3953
|
activePromptCorrelation: undefined,
|
|
3710
3954
|
recordPromptTerminal,
|
|
3711
|
-
notePromptReconciliation:
|
|
3955
|
+
notePromptReconciliation: (correlation, frame) => {
|
|
3956
|
+
void kindReconciliation.noteTransition("prompt", correlation, frame);
|
|
3957
|
+
},
|
|
3712
3958
|
emitPromptFailure,
|
|
3713
3959
|
emitPromptLifecycle,
|
|
3714
3960
|
emitPromptEvent,
|
|
@@ -3774,6 +4020,22 @@ export function createNotificationsExtension(
|
|
|
3774
4020
|
const frame = JSON.parse(inbound.json) as unknown;
|
|
3775
4021
|
if (!frame || typeof frame !== "object") return;
|
|
3776
4022
|
const typedFrame = frame as Record<string, unknown>;
|
|
4023
|
+
if (initializedRuntime.inboundFenced) {
|
|
4024
|
+
if (typedFrame.type === "control_request" && typeof typedFrame.id === "string") {
|
|
4025
|
+
try {
|
|
4026
|
+
server.sendTo(
|
|
4027
|
+
inbound.connectionId,
|
|
4028
|
+
JSON.stringify({
|
|
4029
|
+
type: "control_response",
|
|
4030
|
+
id: typedFrame.id,
|
|
4031
|
+
ok: false,
|
|
4032
|
+
error: { code: "endpoint_stale", message: "session endpoint is draining." },
|
|
4033
|
+
}),
|
|
4034
|
+
);
|
|
4035
|
+
} catch {}
|
|
4036
|
+
}
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
3777
4039
|
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
3778
4040
|
if (typedFrame.type === "event_replay") {
|
|
3779
4041
|
const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
|
|
@@ -3808,7 +4070,12 @@ export function createNotificationsExtension(
|
|
|
3808
4070
|
|
|
3809
4071
|
server.onReply((err, reply) => {
|
|
3810
4072
|
if (err || !reply) return;
|
|
3811
|
-
if (
|
|
4073
|
+
if (
|
|
4074
|
+
runtime?.inboundFenced ||
|
|
4075
|
+
runtime?.stopping ||
|
|
4076
|
+
runtime?.policySuspended ||
|
|
4077
|
+
runtimes.get(id) !== runtime
|
|
4078
|
+
) {
|
|
3812
4079
|
try {
|
|
3813
4080
|
server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
|
|
3814
4081
|
} catch {}
|
|
@@ -4050,6 +4317,7 @@ export function createNotificationsExtension(
|
|
|
4050
4317
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
4051
4318
|
server.onInbound((err, inbound) => {
|
|
4052
4319
|
if (err || !inbound) return;
|
|
4320
|
+
if (initializedRuntime.inboundFenced) return;
|
|
4053
4321
|
const authenticatedInbound = inbound as typeof inbound & {
|
|
4054
4322
|
connectionId: string;
|
|
4055
4323
|
messageId?: number;
|
|
@@ -4642,7 +4910,16 @@ export function createNotificationsExtension(
|
|
|
4642
4910
|
};
|
|
4643
4911
|
|
|
4644
4912
|
api.on("session_start", async (_event, ctx) => {
|
|
4645
|
-
|
|
4913
|
+
const task = startAndReconcileSession(ctx);
|
|
4914
|
+
// Track full start+reconcile so settled startups join replacement-token
|
|
4915
|
+
// reconcile before owner release. Pending native startup (/notify on) stays
|
|
4916
|
+
// nonblocking: shutdown only awaits these tasks when sessionStartPromises is clear.
|
|
4917
|
+
sessionLifecycleTasks.add(task);
|
|
4918
|
+
try {
|
|
4919
|
+
await task;
|
|
4920
|
+
} finally {
|
|
4921
|
+
sessionLifecycleTasks.delete(task);
|
|
4922
|
+
}
|
|
4646
4923
|
});
|
|
4647
4924
|
|
|
4648
4925
|
// A session endpoint's token and generation are authority for exactly one
|
|
@@ -4688,10 +4965,27 @@ export function createNotificationsExtension(
|
|
|
4688
4965
|
});
|
|
4689
4966
|
};
|
|
4690
4967
|
|
|
4968
|
+
const preparePredecessorForTerminal = async (id: string): Promise<void> => {
|
|
4969
|
+
const predecessor = runtimes.get(id);
|
|
4970
|
+
if (!predecessor || cleanupRetries.has(id))
|
|
4971
|
+
throw new Error(`notifications: predecessor runtime ${id} is not safely send-capable.`);
|
|
4972
|
+
predecessor.inboundFenced = true;
|
|
4973
|
+
// Release broker/host authority while leaving the native server alive for
|
|
4974
|
+
// the one accepted terminal response. stopAndWait is deferred to stopSession.
|
|
4975
|
+
const stopped = await predecessor.host.stop();
|
|
4976
|
+
if (stopped !== "stopped" && predecessor.host.started)
|
|
4977
|
+
throw new Error(`notifications: predecessor runtime ${id} host release was not proven.`);
|
|
4978
|
+
predecessor.hostStopped = true;
|
|
4979
|
+
predecessor.brokerRegistrationReleased = !predecessor.brokerRegistrationActive || predecessor.hostStopped;
|
|
4980
|
+
if (predecessor.brokerRegistrationActive && predecessor.hostStopped) predecessor.brokerRegistrationActive = false;
|
|
4981
|
+
predecessor.host.reverse.dispose();
|
|
4982
|
+
};
|
|
4983
|
+
|
|
4691
4984
|
const rotateSessionAuthority = async (
|
|
4692
4985
|
event: { previousSessionFile?: string },
|
|
4693
4986
|
ctx: ExtensionContext,
|
|
4694
4987
|
awaitStartup: boolean,
|
|
4988
|
+
options: { deferPredecessorStop?: boolean } = {},
|
|
4695
4989
|
): Promise<void> => {
|
|
4696
4990
|
if (extensionShuttingDown) return;
|
|
4697
4991
|
const newId = sessionId(ctx);
|
|
@@ -4700,7 +4994,11 @@ export function createNotificationsExtension(
|
|
|
4700
4994
|
const pendingStartup = sessionStartPromises.get(newId);
|
|
4701
4995
|
if (pendingStartup) {
|
|
4702
4996
|
if (awaitStartup) {
|
|
4703
|
-
await pendingStartup;
|
|
4997
|
+
const result = await pendingStartup;
|
|
4998
|
+
if (!lifecycleStartupCapability && result.status === "failed")
|
|
4999
|
+
throw new Error(
|
|
5000
|
+
`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`,
|
|
5001
|
+
);
|
|
4704
5002
|
if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
|
|
4705
5003
|
await controller.reconcileCurrentSession(ctx);
|
|
4706
5004
|
} else {
|
|
@@ -4715,14 +5013,14 @@ export function createNotificationsExtension(
|
|
|
4715
5013
|
}
|
|
4716
5014
|
if (prevId && prevId !== newId) {
|
|
4717
5015
|
controller.rekeySession(prevId, newId);
|
|
4718
|
-
|
|
4719
|
-
await
|
|
4720
|
-
}
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
5016
|
+
if (options.deferPredecessorStop) {
|
|
5017
|
+
await preparePredecessorForTerminal(prevId);
|
|
5018
|
+
} else {
|
|
5019
|
+
const stopped = await stopSession(prevId);
|
|
5020
|
+
if (cleanupRetries.has(prevId) || runtimes.has(prevId) || sessionStartPromises.has(prevId))
|
|
5021
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release is uncertain.`);
|
|
5022
|
+
if (!stopped && activeRuntimeId === prevId)
|
|
5023
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release was not proven.`);
|
|
4726
5024
|
}
|
|
4727
5025
|
}
|
|
4728
5026
|
if (extensionShuttingDown) return;
|
|
@@ -4743,18 +5041,19 @@ export function createNotificationsExtension(
|
|
|
4743
5041
|
trackBranchStartup(newId, ctx, startup);
|
|
4744
5042
|
};
|
|
4745
5043
|
api.on("session_switch", async (event, ctx) => {
|
|
5044
|
+
const awaitStartup = shouldAwaitNotificationStartup(event);
|
|
4746
5045
|
if (identityControlInFlight) {
|
|
4747
|
-
deferredIdentityRotation = { event, ctx, awaitStartup
|
|
5046
|
+
deferredIdentityRotation = { event, ctx, awaitStartup };
|
|
4748
5047
|
return;
|
|
4749
5048
|
}
|
|
4750
|
-
await rotateSessionAuthority(event, ctx,
|
|
5049
|
+
await rotateSessionAuthority(event, ctx, awaitStartup);
|
|
4751
5050
|
});
|
|
4752
5051
|
api.on("session_branch", async (event, ctx) => {
|
|
4753
5052
|
if (identityControlInFlight) {
|
|
4754
|
-
deferredIdentityRotation = { event, ctx, awaitStartup:
|
|
5053
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: true };
|
|
4755
5054
|
return;
|
|
4756
5055
|
}
|
|
4757
|
-
await rotateSessionAuthority(event, ctx,
|
|
5056
|
+
await rotateSessionAuthority(event, ctx, true);
|
|
4758
5057
|
});
|
|
4759
5058
|
|
|
4760
5059
|
const terminalizeInFlightTools = (
|
|
@@ -5292,8 +5591,12 @@ export function createNotificationsExtension(
|
|
|
5292
5591
|
extensionShuttingDown = true;
|
|
5293
5592
|
identityControlInFlight = false;
|
|
5294
5593
|
deferredIdentityRotation = undefined;
|
|
5295
|
-
await Promise.allSettled([...branchStartupTasks]);
|
|
5296
5594
|
const id = sessionId(ctx);
|
|
5595
|
+
// Join settled start+reconcile before owner release. Keep pending native
|
|
5596
|
+
// startup (/notify on) nonblocking — do not await sessionLifecycleTasks while
|
|
5597
|
+
// sessionStartPromises still has this id.
|
|
5598
|
+
if (!sessionStartPromises.has(id)) await Promise.allSettled([...sessionLifecycleTasks]);
|
|
5599
|
+
await Promise.allSettled([...branchStartupTasks]);
|
|
5297
5600
|
const rt = runtimes.get(id);
|
|
5298
5601
|
if (rt) terminalizeInFlightTools(rt, id, "cancelled");
|
|
5299
5602
|
// Startup is only genuinely in flight when a `sessionStartPromises` entry
|