@gajae-code/coding-agent 0.11.10 → 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 +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- 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-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -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 +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -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/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -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 +22 -9
- 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/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -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/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- 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/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- 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/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -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/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -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/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -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 +23 -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 +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- 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/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- 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/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- 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 +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- 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 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- 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 +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- 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 +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- 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 +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- 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/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -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/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +107 -7
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- 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/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- 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 +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -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
|
@@ -68,17 +68,24 @@ type ConnectionTask = {
|
|
|
68
68
|
|
|
69
69
|
const STARTUP_TIMEOUT_MS = 250;
|
|
70
70
|
const STARTUP_TIMEOUT_GRACE_MS = 500;
|
|
71
|
+
/**
|
|
72
|
+
* Default ceiling on how long `discoverAndConnect` waits for a server batch to
|
|
73
|
+
* come up. Deliberately short: a config with a large `timeout` must not be able
|
|
74
|
+
* to hang ordinary startup. ACP lifecycle launches carry their own, larger
|
|
75
|
+
* budget derived from the readiness deadline (see `maxStartupTimeoutMs`).
|
|
76
|
+
*/
|
|
71
77
|
const MAX_STARTUP_TIMEOUT_MS = 1_750;
|
|
72
78
|
|
|
73
|
-
function resolveStartupTimeoutMs(configs: MCPServerConfig[]): number {
|
|
79
|
+
export function resolveStartupTimeoutMs(configs: MCPServerConfig[], maxStartupTimeoutMs?: number): number {
|
|
80
|
+
const ceiling =
|
|
81
|
+
typeof maxStartupTimeoutMs === "number" && Number.isFinite(maxStartupTimeoutMs) && maxStartupTimeoutMs > 0
|
|
82
|
+
? Math.max(STARTUP_TIMEOUT_MS, maxStartupTimeoutMs)
|
|
83
|
+
: MAX_STARTUP_TIMEOUT_MS;
|
|
74
84
|
const configuredTimeouts = configs
|
|
75
85
|
.map(config => config.timeout)
|
|
76
86
|
.filter((timeout): timeout is number => typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0);
|
|
77
87
|
if (configuredTimeouts.length === 0) return STARTUP_TIMEOUT_MS;
|
|
78
|
-
return Math.min(
|
|
79
|
-
MAX_STARTUP_TIMEOUT_MS,
|
|
80
|
-
Math.max(STARTUP_TIMEOUT_MS, Math.max(...configuredTimeouts) + STARTUP_TIMEOUT_GRACE_MS),
|
|
81
|
-
);
|
|
88
|
+
return Math.min(ceiling, Math.max(STARTUP_TIMEOUT_MS, Math.max(...configuredTimeouts) + STARTUP_TIMEOUT_GRACE_MS));
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
@@ -185,6 +192,13 @@ export interface MCPDiscoverOptions {
|
|
|
185
192
|
export interface MCPManagerOptions {
|
|
186
193
|
/** Restrict this instance to tools from an explicit MCP config. */
|
|
187
194
|
toolsOnly?: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* Ceiling for the startup wait, in milliseconds. Only ACP lifecycle launches
|
|
197
|
+
* set this, so a slow ACP MCP handshake gets the readiness budget while every
|
|
198
|
+
* other consumer keeps the short default. Non-positive or non-finite values
|
|
199
|
+
* are ignored and the default applies.
|
|
200
|
+
*/
|
|
201
|
+
maxStartupTimeoutMs?: number;
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
/**
|
|
@@ -269,12 +283,15 @@ export class MCPManager {
|
|
|
269
283
|
);
|
|
270
284
|
}
|
|
271
285
|
|
|
286
|
+
readonly #maxStartupTimeoutMs: number | undefined;
|
|
287
|
+
|
|
272
288
|
constructor(
|
|
273
289
|
private cwd: string,
|
|
274
290
|
private toolCache: MCPToolCache | null = null,
|
|
275
291
|
options: MCPManagerOptions = {},
|
|
276
292
|
) {
|
|
277
293
|
this.#toolsOnly = options.toolsOnly === true;
|
|
294
|
+
this.#maxStartupTimeoutMs = options.maxStartupTimeoutMs;
|
|
278
295
|
}
|
|
279
296
|
|
|
280
297
|
isToolsOnly(): boolean {
|
|
@@ -633,7 +650,10 @@ export class MCPManager {
|
|
|
633
650
|
}
|
|
634
651
|
|
|
635
652
|
if (connectionTasks.length > 0) {
|
|
636
|
-
const startupTimeoutMs = resolveStartupTimeoutMs(
|
|
653
|
+
const startupTimeoutMs = resolveStartupTimeoutMs(
|
|
654
|
+
connectionTasks.map(task => task.config),
|
|
655
|
+
this.#maxStartupTimeoutMs,
|
|
656
|
+
);
|
|
637
657
|
const firstUnexpectedFailure = Promise.withResolvers<{ reason: unknown }>();
|
|
638
658
|
if (this.#toolsOnly) {
|
|
639
659
|
for (const task of connectionTasks) {
|
|
@@ -2,9 +2,18 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { isEnoent, logger } from "@gajae-code/utils";
|
|
4
4
|
import { getAgentDir } from "@gajae-code/utils/dirs";
|
|
5
|
+
import { $credentialEnv } from "@gajae-code/utils/env";
|
|
5
6
|
|
|
6
7
|
const SMITHERY_AUTH_FILENAME = "smithery.json";
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Smithery web origin, from trusted environment sources only.
|
|
10
|
+
*
|
|
11
|
+
* This origin serves the CLI auth session and the verification URL the user is
|
|
12
|
+
* sent to, so whatever can set it can drive the login flow. `$env` merges the
|
|
13
|
+
* caller's `cwd/.env` into `process.env`, so reading it there would let
|
|
14
|
+
* repository content redirect that flow.
|
|
15
|
+
*/
|
|
16
|
+
const SMITHERY_URL = $credentialEnv("SMITHERY_URL") || "https://smithery.ai";
|
|
8
17
|
|
|
9
18
|
type SmitheryCliAuthSession = {
|
|
10
19
|
sessionId: string;
|
|
@@ -31,6 +40,11 @@ function normalizeApiKey(value: string | undefined): string | undefined {
|
|
|
31
40
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
/** Test seam: the Smithery origin and env API key as resolved from trusted env. */
|
|
44
|
+
export function resolveSmitheryEnvForTest(): { url: string; apiKey: string | undefined } {
|
|
45
|
+
return { url: SMITHERY_URL, apiKey: normalizeApiKey($credentialEnv("SMITHERY_API_KEY")) };
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
export function getSmitheryLoginUrl(): string {
|
|
35
49
|
return SMITHERY_URL;
|
|
36
50
|
}
|
|
@@ -62,7 +76,8 @@ export async function pollSmitheryCliAuthSession(
|
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
export async function getSmitheryApiKey(): Promise<string | undefined> {
|
|
65
|
-
|
|
79
|
+
// Trusted sources only: this key authenticates every Smithery API call.
|
|
80
|
+
const envKey = normalizeApiKey($credentialEnv("SMITHERY_API_KEY"));
|
|
66
81
|
if (envKey) return envKey;
|
|
67
82
|
|
|
68
83
|
const authPath = getSmitheryAuthPath();
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { $credentialEnv } from "@gajae-code/utils/env";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Smithery API base, from trusted environment sources only.
|
|
5
|
+
*
|
|
6
|
+
* Every request to this base carries `Authorization: Bearer <apiKey>` and the
|
|
7
|
+
* `/connect` routes return the connection records the agent consumes, so
|
|
8
|
+
* whatever can set it receives the key and supplies those records. `$env` merges
|
|
9
|
+
* the caller's `cwd/.env` into `process.env`.
|
|
10
|
+
*/
|
|
11
|
+
const SMITHERY_API_BASE_URL = ($credentialEnv("SMITHERY_API_URL") || "https://api.smithery.ai").replace(/\/+$/, "");
|
|
2
12
|
|
|
3
13
|
export class SmitheryConnectError extends Error {
|
|
4
14
|
status: number;
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -179,7 +179,14 @@ export class AcpSdkAdapter {
|
|
|
179
179
|
this.#assertGenericDisposition("global", operation);
|
|
180
180
|
if (isLifecycleOperation(operation) && !idempotencyKey)
|
|
181
181
|
throw new AcpSdkAdapterError("invalid_input", "idempotencyKey is required for lifecycle operations.");
|
|
182
|
-
|
|
182
|
+
const readinessTimeoutMs =
|
|
183
|
+
typeof input.readinessTimeoutMs === "number" && Number.isSafeInteger(input.readinessTimeoutMs)
|
|
184
|
+
? input.readinessTimeoutMs
|
|
185
|
+
: undefined;
|
|
186
|
+
return await this.#client.global(operation, input, {
|
|
187
|
+
idempotencyKey,
|
|
188
|
+
...(readinessTimeoutMs ? { timeoutMs: readinessTimeoutMs + 1_000 } : {}),
|
|
189
|
+
});
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
async sdkControl(params: { operation: string; input?: JsonObject }): Promise<unknown> {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const ACP_FINAL_TEXT_LIMIT = 100_000;
|
|
2
|
+
|
|
3
|
+
export interface BoundedAcpFinalText {
|
|
4
|
+
text: string;
|
|
5
|
+
truncated: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type AcpFinalTextResolution =
|
|
9
|
+
| { kind: "none"; final: BoundedAcpFinalText }
|
|
10
|
+
| { kind: "emit"; final: BoundedAcpFinalText; text: string }
|
|
11
|
+
| { kind: "divergent"; final: BoundedAcpFinalText };
|
|
12
|
+
|
|
13
|
+
export function boundAcpFinalText(value: string): BoundedAcpFinalText {
|
|
14
|
+
if (value.length <= ACP_FINAL_TEXT_LIMIT) return { text: value, truncated: false };
|
|
15
|
+
let text = value.slice(0, ACP_FINAL_TEXT_LIMIT);
|
|
16
|
+
const finalCodeUnit = text.charCodeAt(text.length - 1);
|
|
17
|
+
if (finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff) text = text.slice(0, -1);
|
|
18
|
+
return { text, truncated: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function acpFinalTextFromMessage(message: unknown): BoundedAcpFinalText {
|
|
22
|
+
const content = (message as { content?: unknown } | null | undefined)?.content;
|
|
23
|
+
if (typeof content === "string") return boundAcpFinalText(content);
|
|
24
|
+
if (!Array.isArray(content)) return { text: "", truncated: false };
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
for (const block of content) {
|
|
27
|
+
if (!block || typeof block !== "object" || (block as { type?: unknown }).type !== "text") continue;
|
|
28
|
+
const text = (block as { text?: unknown }).text;
|
|
29
|
+
if (typeof text === "string") parts.push(text);
|
|
30
|
+
}
|
|
31
|
+
return boundAcpFinalText(parts.join(""));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveAcpFinalText(streamed: string, finalText: string): AcpFinalTextResolution {
|
|
35
|
+
const final = boundAcpFinalText(finalText);
|
|
36
|
+
if (!final.text || streamed === final.text || streamed.includes(final.text)) return { kind: "none", final };
|
|
37
|
+
if (!streamed) return { kind: "emit", final, text: final.text };
|
|
38
|
+
if (final.text.startsWith(streamed)) return { kind: "emit", final, text: final.text.slice(streamed.length) };
|
|
39
|
+
return { kind: "divergent", final };
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const ACP_MCP_REQUEST_TIMEOUT_MS = 30_000;
|
|
2
|
+
export const ACP_MCP_LIFECYCLE_TIMEOUT_MS = ACP_MCP_REQUEST_TIMEOUT_MS + 500;
|
|
3
|
+
/**
|
|
4
|
+
* Slice of the readiness budget reserved for the work that follows MCP startup
|
|
5
|
+
* (session wiring, marker publication). The ACP MCP startup ceiling is the
|
|
6
|
+
* remaining time to `semanticReadyDeadlineAt` minus this headroom, so a slow
|
|
7
|
+
* MCP handshake cannot consume the whole readiness window.
|
|
8
|
+
*/
|
|
9
|
+
export const ACP_MCP_STARTUP_HEADROOM_MS = 250;
|
|
10
|
+
|
|
11
|
+
export interface SessionLifecycleMcpStdioServer {
|
|
12
|
+
type?: "stdio";
|
|
13
|
+
name: string;
|
|
14
|
+
command: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SessionLifecycleMcpRemoteServer {
|
|
20
|
+
type: "http" | "sse";
|
|
21
|
+
name: string;
|
|
22
|
+
url: string;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SessionLifecycleMcpServer = SessionLifecycleMcpStdioServer | SessionLifecycleMcpRemoteServer;
|
|
@@ -5,7 +5,7 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import type { NativeExactUnlinkResult } from "@gajae-code/natives";
|
|
7
7
|
import * as native from "@gajae-code/natives";
|
|
8
|
-
import { resolveEquivalentPath } from "@gajae-code/utils";
|
|
8
|
+
import { $credentialEnv, resolveEquivalentPath } from "@gajae-code/utils";
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
isModelProfileError,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type VerifiedSessionDeleteResult,
|
|
30
30
|
type VerifiedSessionDeleteTarget,
|
|
31
31
|
} from "../../session/session-storage";
|
|
32
|
+
import type { SessionLifecycleMcpServer } from "../acp/mcp";
|
|
32
33
|
import { SdkClient, SdkClientError } from "../client/client";
|
|
33
34
|
import {
|
|
34
35
|
type LogicalSessionCandidate,
|
|
@@ -215,6 +216,7 @@ export interface SessionLifecycleLaunchRequest {
|
|
|
215
216
|
/** Broker-issued effect marker which the child echoes only after host readiness. */
|
|
216
217
|
effectMarker?: string;
|
|
217
218
|
modelPreset?: string;
|
|
219
|
+
mcpServers?: SessionLifecycleMcpServer[];
|
|
218
220
|
worktree?: SessionLifecycleWorktreeTarget;
|
|
219
221
|
receivedAt: number;
|
|
220
222
|
requestedReadinessTimeoutMs: number;
|
|
@@ -248,6 +250,77 @@ function hasValidTranscriptAuthority(path: unknown, identity: unknown): path is
|
|
|
248
250
|
return typeof path === "string" && path.length > 0 && isSessionLifecycleTranscriptIdentity(identity);
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
function isSessionLifecycleMcpServer(value: unknown): value is SessionLifecycleMcpServer {
|
|
254
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
255
|
+
const server = value as Record<string, unknown>;
|
|
256
|
+
if (server.type === "http" || server.type === "sse") {
|
|
257
|
+
if (
|
|
258
|
+
!Object.keys(server).every(key => key === "type" || key === "name" || key === "url" || key === "headers") ||
|
|
259
|
+
typeof server.name !== "string" ||
|
|
260
|
+
!/^[A-Za-z0-9_.-]{1,100}$/.test(server.name) ||
|
|
261
|
+
typeof server.url !== "string" ||
|
|
262
|
+
server.url.length > 8_192
|
|
263
|
+
)
|
|
264
|
+
return false;
|
|
265
|
+
try {
|
|
266
|
+
const url = new URL(server.url);
|
|
267
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (server.headers === undefined) return true;
|
|
272
|
+
if (typeof server.headers !== "object" || server.headers === null || Array.isArray(server.headers)) return false;
|
|
273
|
+
const headers = server.headers as Record<string, unknown>;
|
|
274
|
+
return (
|
|
275
|
+
Object.keys(headers).length <= 100 &&
|
|
276
|
+
Object.entries(headers).every(
|
|
277
|
+
([name, headerValue]) =>
|
|
278
|
+
name.length > 0 &&
|
|
279
|
+
name.length <= 256 &&
|
|
280
|
+
!name.includes("\r") &&
|
|
281
|
+
!name.includes("\n") &&
|
|
282
|
+
typeof headerValue === "string" &&
|
|
283
|
+
headerValue.length <= 8_192 &&
|
|
284
|
+
!headerValue.includes("\r") &&
|
|
285
|
+
!headerValue.includes("\n"),
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const env = server.env;
|
|
290
|
+
return (
|
|
291
|
+
Object.keys(server).every(
|
|
292
|
+
key => key === "type" || key === "name" || key === "command" || key === "args" || key === "env",
|
|
293
|
+
) &&
|
|
294
|
+
(server.type === undefined || server.type === "stdio") &&
|
|
295
|
+
typeof server.name === "string" &&
|
|
296
|
+
/^[A-Za-z0-9_.-]{1,100}$/.test(server.name) &&
|
|
297
|
+
typeof server.command === "string" &&
|
|
298
|
+
server.command.length <= 4_096 &&
|
|
299
|
+
path.isAbsolute(server.command) &&
|
|
300
|
+
Array.isArray(server.args) &&
|
|
301
|
+
server.args.length <= 100 &&
|
|
302
|
+
server.args.every(argument => typeof argument === "string" && argument.length <= 8_192) &&
|
|
303
|
+
(env === undefined ||
|
|
304
|
+
(typeof env === "object" &&
|
|
305
|
+
env !== null &&
|
|
306
|
+
!Array.isArray(env) &&
|
|
307
|
+
Object.keys(env).length <= 100 &&
|
|
308
|
+
Object.entries(env).every(
|
|
309
|
+
([name, envValue]) =>
|
|
310
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && typeof envValue === "string" && envValue.length <= 32_768,
|
|
311
|
+
)))
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isSessionLifecycleMcpServers(value: unknown): value is SessionLifecycleMcpServer[] {
|
|
316
|
+
return (
|
|
317
|
+
Array.isArray(value) &&
|
|
318
|
+
value.length <= 64 &&
|
|
319
|
+
value.every(isSessionLifecycleMcpServer) &&
|
|
320
|
+
new Set(value.map(server => server.name)).size === value.length
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
251
324
|
export function readSessionLifecycleLaunchRequest(
|
|
252
325
|
value: string | undefined,
|
|
253
326
|
now = Date.now(),
|
|
@@ -278,6 +351,7 @@ export function readSessionLifecycleLaunchRequest(
|
|
|
278
351
|
(request.effectMarker !== undefined &&
|
|
279
352
|
(typeof request.effectMarker !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(request.effectMarker))) ||
|
|
280
353
|
(request.modelPreset !== undefined && (typeof request.modelPreset !== "string" || !request.modelPreset)) ||
|
|
354
|
+
(request.mcpServers !== undefined && !isSessionLifecycleMcpServers(request.mcpServers)) ||
|
|
281
355
|
!hasValidLifecycleDeadlines(
|
|
282
356
|
{
|
|
283
357
|
receivedAt: request.receivedAt as number,
|
|
@@ -310,6 +384,7 @@ type SessionLaunch = {
|
|
|
310
384
|
sessionPath?: string;
|
|
311
385
|
sessionIdentity?: SessionLifecycleTranscriptIdentity;
|
|
312
386
|
modelPreset?: string;
|
|
387
|
+
mcpServers?: SessionLifecycleMcpServer[];
|
|
313
388
|
worktree?: SessionLifecycleWorktreeTarget;
|
|
314
389
|
worktreePlan?: GjcLaunchWorktreePlan;
|
|
315
390
|
};
|
|
@@ -631,12 +706,31 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
|
|
|
631
706
|
});
|
|
632
707
|
}
|
|
633
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Operator override for the session-host command, resolved from trusted
|
|
711
|
+
* environment sources only.
|
|
712
|
+
*
|
|
713
|
+
* The result is spawned directly, so whatever can set it chooses which binary
|
|
714
|
+
* the broker runs. `$env` merges the caller's `cwd/.env` into `process.env`, so
|
|
715
|
+
* reading it there would let repository content replace the session host;
|
|
716
|
+
* resolve it the same way provider credentials are (launching shell plus
|
|
717
|
+
* GJC/user-owned `.env` files, never the project `.env`).
|
|
718
|
+
*/
|
|
719
|
+
function sdkSessionCommandOverride(): { file: string; args: string[] } | undefined {
|
|
720
|
+
const configured = $credentialEnv("GJC_SDK_SESSION_COMMAND");
|
|
721
|
+
if (!configured) return undefined;
|
|
722
|
+
const [file, ...args] = configured.trim().split(/\s+/);
|
|
723
|
+
return file ? { file, args } : undefined;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Test seam: the session-host command override as resolved from trusted env. */
|
|
727
|
+
export function sdkSessionCommandOverrideForTest(): { file: string; args: string[] } | undefined {
|
|
728
|
+
return sdkSessionCommandOverride();
|
|
729
|
+
}
|
|
730
|
+
|
|
634
731
|
function command(broker: Broker): LifecycleCommand {
|
|
635
|
-
const configured =
|
|
636
|
-
if (configured)
|
|
637
|
-
const [file, ...args] = configured.trim().split(/\s+/);
|
|
638
|
-
if (file) return { file, args };
|
|
639
|
-
}
|
|
732
|
+
const configured = sdkSessionCommandOverride();
|
|
733
|
+
if (configured) return configured;
|
|
640
734
|
return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
|
|
641
735
|
}
|
|
642
736
|
|
|
@@ -2299,9 +2393,12 @@ async function launchInput(
|
|
|
2299
2393
|
if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
|
|
2300
2394
|
return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
|
|
2301
2395
|
const modelPreset = text(input.modelPreset);
|
|
2396
|
+
if (input.mcpServers !== undefined && !isSessionLifecycleMcpServers(input.mcpServers))
|
|
2397
|
+
return fail("invalid_input", "mcpServers must contain unique valid stdio, HTTP, or SSE server definitions.");
|
|
2398
|
+
const mcpServers = input.mcpServers as SessionLifecycleMcpServer[] | undefined;
|
|
2302
2399
|
|
|
2303
2400
|
if (operation === "session.create")
|
|
2304
|
-
return { id: randomUUID(), cwd, root: resolvedRoot, modelPreset, worktree, worktreePlan };
|
|
2401
|
+
return { id: randomUUID(), cwd, root: resolvedRoot, modelPreset, mcpServers, worktree, worktreePlan };
|
|
2305
2402
|
if (operation === "session.resume") {
|
|
2306
2403
|
if (!requested) return fail("invalid_input", "sessionId is required to resume a saved session.");
|
|
2307
2404
|
const savedPath = text(input.sessionPath);
|
|
@@ -2315,6 +2412,7 @@ async function launchInput(
|
|
|
2315
2412
|
sessionPath: saved.path,
|
|
2316
2413
|
sessionIdentity: saved.identity,
|
|
2317
2414
|
modelPreset,
|
|
2415
|
+
mcpServers,
|
|
2318
2416
|
worktree,
|
|
2319
2417
|
worktreePlan,
|
|
2320
2418
|
};
|
|
@@ -2336,6 +2434,7 @@ async function launchInput(
|
|
|
2336
2434
|
sourceSessionIdentity: source.identity,
|
|
2337
2435
|
sourceCwd,
|
|
2338
2436
|
modelPreset,
|
|
2437
|
+
mcpServers,
|
|
2339
2438
|
worktree,
|
|
2340
2439
|
worktreePlan,
|
|
2341
2440
|
};
|
|
@@ -2805,6 +2904,7 @@ async function executeLifecycleResponse(
|
|
|
2805
2904
|
...(launch.sessionPath ? { sessionPath: launch.sessionPath } : {}),
|
|
2806
2905
|
...(launch.sessionIdentity ? { sessionIdentity: launch.sessionIdentity } : {}),
|
|
2807
2906
|
...(launch.modelPreset ? { modelPreset: launch.modelPreset } : {}),
|
|
2907
|
+
...(launch.mcpServers ? { mcpServers: launch.mcpServers } : {}),
|
|
2808
2908
|
...(launch.worktree ? { worktree: launch.worktree } : {}),
|
|
2809
2909
|
};
|
|
2810
2910
|
let child: ChildProcess | undefined;
|
|
@@ -67,7 +67,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
|
|
|
67
67
|
idleTimeoutMs: 60_000,
|
|
68
68
|
rich: { enabled: true },
|
|
69
69
|
richDraft: { enabled: false },
|
|
70
|
-
toolActivity: { enabled:
|
|
70
|
+
toolActivity: { enabled: false },
|
|
71
71
|
topics: { nameTemplate: undefined },
|
|
72
72
|
btw: { enabled: true },
|
|
73
73
|
streaming: { enabled: true },
|
|
@@ -102,7 +102,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
|
|
|
102
102
|
idleTimeoutMs: 60_000,
|
|
103
103
|
rich: { enabled: true },
|
|
104
104
|
richDraft: { enabled: false },
|
|
105
|
-
toolActivity: { enabled:
|
|
105
|
+
toolActivity: { enabled: false },
|
|
106
106
|
topics: { nameTemplate: undefined },
|
|
107
107
|
btw: { enabled: true },
|
|
108
108
|
streaming: { enabled: true },
|
|
@@ -34,11 +34,12 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
34
34
|
* concrete detached quarantine plus a proven-absent canonical lock pathname —
|
|
35
35
|
* when deleting an observed owner-lock lease. Generation 12 refreshes retained
|
|
36
36
|
* native path and process authority semantics. Generation 13 rejects special
|
|
37
|
-
* files before retained native authority opens.
|
|
37
|
+
* files before retained native authority opens. Generation 14 reloads shared
|
|
38
|
+
* chat daemons after notification configuration parsing changes.
|
|
38
39
|
*/
|
|
39
40
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
40
|
-
discord:
|
|
41
|
-
slack:
|
|
41
|
+
discord: 14,
|
|
42
|
+
slack: 14,
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
package/src/sdk/bus/config.ts
CHANGED
|
@@ -170,7 +170,7 @@ export function parseNotificationSettingsSnapshot(rawConfig?: unknown): Notifica
|
|
|
170
170
|
enabled: notificationSettingsBoolean(richDraft.enabled, false),
|
|
171
171
|
},
|
|
172
172
|
toolActivity: {
|
|
173
|
-
enabled: notificationSettingsBoolean(toolActivity.enabled,
|
|
173
|
+
enabled: notificationSettingsBoolean(toolActivity.enabled, false),
|
|
174
174
|
},
|
|
175
175
|
streaming: {
|
|
176
176
|
enabled: notificationSettingsBoolean(streaming.enabled, true),
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordering seam for an in-band SDK identity transition.
|
|
3
|
+
*
|
|
4
|
+
* A request arrives through predecessor A, so a successful terminal response cannot
|
|
5
|
+
* be published until successor B is ready. Native builds may provide a true
|
|
6
|
+
* send-only connection lease; source/dev builds use the conservative fallback in
|
|
7
|
+
* which the predecessor server remains alive until the terminal write completes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type TerminalSendOutcome = "written" | "disconnected" | "write_failed";
|
|
11
|
+
|
|
12
|
+
/** Fence/capability hooks may return false to reject; undefined means ok. */
|
|
13
|
+
export type IdentityControlGateResult = boolean | undefined;
|
|
14
|
+
export type IdentityControlGate = () => IdentityControlGateResult | Promise<IdentityControlGateResult>;
|
|
15
|
+
|
|
16
|
+
export interface IdentityControlSuccessPathInput {
|
|
17
|
+
/** Fence predecessor inbound work. The fence must not close its response writer. */
|
|
18
|
+
fence: IdentityControlGate;
|
|
19
|
+
/** Optional explicit proof that the predecessor writer is still usable. */
|
|
20
|
+
ensurePredecessorSendCapable?: IdentityControlGate;
|
|
21
|
+
/** Start successor B and resolve only after its public endpoint is ready. */
|
|
22
|
+
startSuccessor: () => Promise<void>;
|
|
23
|
+
/** Write and flush either the success or non-success terminal response. */
|
|
24
|
+
sendTerminal: () => Promise<TerminalSendOutcome>;
|
|
25
|
+
/** Release predecessor A after the terminal response has settled. */
|
|
26
|
+
stopPredecessor: () => Promise<void>;
|
|
27
|
+
/** Set only when a caller cannot use the retained-writer fallback. */
|
|
28
|
+
requireNativeControlDrain?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IdentityControlTerminalPathInput {
|
|
32
|
+
fenceInbound: IdentityControlGate;
|
|
33
|
+
ensurePredecessorSendCapable?: IdentityControlGate;
|
|
34
|
+
startSuccessorReady: () => Promise<void>;
|
|
35
|
+
sendTerminal: () => Promise<TerminalSendOutcome>;
|
|
36
|
+
stopPredecessor: () => Promise<void>;
|
|
37
|
+
requireNativeControlDrain?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Probe the loaded native addon, never an environment flag or a TypeScript shim.
|
|
42
|
+
* Older/source installs intentionally report false so callers can select the
|
|
43
|
+
* retained-writer fallback or fail closed when detach is mandatory.
|
|
44
|
+
*/
|
|
45
|
+
export function isNativeControlDrainAvailable(): boolean {
|
|
46
|
+
try {
|
|
47
|
+
// Keep the addon lazy: importing the bus must continue to work without a
|
|
48
|
+
// compiled native artifact (tests and source distributions do that often).
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const natives = require("@gajae-code/natives") as {
|
|
51
|
+
NotificationServer?: {
|
|
52
|
+
prototype?: Record<string, unknown>;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const prototype = natives.NotificationServer?.prototype;
|
|
56
|
+
return (
|
|
57
|
+
typeof prototype?.acquireControlDrain === "function" &&
|
|
58
|
+
typeof prototype?.sendTerminalAndWait === "function" &&
|
|
59
|
+
typeof prototype?.closeControlDrain === "function"
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run the only safe ordering for a successful identity-control response.
|
|
68
|
+
*
|
|
69
|
+
* The `finally` is deliberately around both successor startup and terminal
|
|
70
|
+
* delivery: a failed startup must not leave the fenced predecessor alive, and a
|
|
71
|
+
* failed/disconnected terminal write must still release it. When native detach
|
|
72
|
+
* is unavailable the caller is expected to keep A's server up (not call
|
|
73
|
+
* `stopAndWait`) until `sendTerminal` settles.
|
|
74
|
+
*/
|
|
75
|
+
export async function runIdentityControlSuccessPath(
|
|
76
|
+
input: IdentityControlSuccessPathInput,
|
|
77
|
+
): Promise<TerminalSendOutcome> {
|
|
78
|
+
if (input.requireNativeControlDrain && !isNativeControlDrainAvailable())
|
|
79
|
+
throw new Error("SDK identity control requires the native control-drain lease.");
|
|
80
|
+
|
|
81
|
+
if ((await input.fence()) === false) throw new Error("SDK identity control predecessor fence was not acquired.");
|
|
82
|
+
if ((await input.ensurePredecessorSendCapable?.()) === false)
|
|
83
|
+
throw new Error("SDK identity control predecessor writer is not send-capable.");
|
|
84
|
+
try {
|
|
85
|
+
await input.startSuccessor();
|
|
86
|
+
return await input.sendTerminal();
|
|
87
|
+
} finally {
|
|
88
|
+
await input.stopPredecessor();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Compatibility spelling used by the host wiring design notes. */
|
|
93
|
+
export async function runIdentityControlTerminalPath(
|
|
94
|
+
input: IdentityControlTerminalPathInput,
|
|
95
|
+
): Promise<TerminalSendOutcome> {
|
|
96
|
+
return runIdentityControlSuccessPath({
|
|
97
|
+
fence: input.fenceInbound,
|
|
98
|
+
ensurePredecessorSendCapable: input.ensurePredecessorSendCapable,
|
|
99
|
+
startSuccessor: input.startSuccessorReady,
|
|
100
|
+
sendTerminal: input.sendTerminal,
|
|
101
|
+
stopPredecessor: input.stopPredecessor,
|
|
102
|
+
requireNativeControlDrain: input.requireNativeControlDrain,
|
|
103
|
+
});
|
|
104
|
+
}
|