@gajae-code/coding-agent 0.15.6 → 0.16.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 +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/session-runtime.d.ts +52 -0
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +186 -41
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
|
@@ -2,17 +2,27 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { replaceTabs, truncateToWidth } from "@gajae-code/tui";
|
|
5
6
|
import { getAgentDir } from "@gajae-code/utils";
|
|
6
7
|
import { repo as resolveGitRepository } from "../../utils/git";
|
|
7
8
|
import { ensureBroker } from "../broker/ensure";
|
|
9
|
+
import { resolveSessionLocator } from "../broker/session-index";
|
|
10
|
+
import {
|
|
11
|
+
resolveScopeRequest,
|
|
12
|
+
type ScopeNameV1,
|
|
13
|
+
type ScopeRequestV1,
|
|
14
|
+
type SdkSearchResultV1,
|
|
15
|
+
type SdkSearchRowV1,
|
|
16
|
+
} from "../broker/session-scope";
|
|
8
17
|
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
9
|
-
import { SdkClientError } from "../client";
|
|
18
|
+
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../client";
|
|
10
19
|
import { createBrokerSessionLifecycleService } from "../lifecycle/broker-client";
|
|
11
20
|
import type {
|
|
12
21
|
SessionLifecycleMutationRequest,
|
|
13
22
|
SessionLifecycleOperation,
|
|
14
23
|
SessionLifecycleSavedSession,
|
|
15
24
|
SessionLifecycleSavedSessionIdentity,
|
|
25
|
+
SessionLifecycleService,
|
|
16
26
|
SessionReconcileUncertainTarget,
|
|
17
27
|
} from "../lifecycle/service";
|
|
18
28
|
import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
|
|
@@ -24,6 +34,7 @@ import {
|
|
|
24
34
|
import { adapterDispositionError, findOperation, type OperationKind } from "../protocol/operation-registry";
|
|
25
35
|
import { type SessionAttachment, SessionRouter, SessionRouterError, type SessionRouterFrame } from "../router";
|
|
26
36
|
import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../session-list";
|
|
37
|
+
import { SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
|
|
27
38
|
import {
|
|
28
39
|
type SdkCheckpointRecordV1,
|
|
29
40
|
type SdkRetentionGapV1,
|
|
@@ -39,6 +50,7 @@ import {
|
|
|
39
50
|
|
|
40
51
|
export type SdkSessionCliAction =
|
|
41
52
|
| "list"
|
|
53
|
+
| "search"
|
|
42
54
|
| "inspect"
|
|
43
55
|
| "send"
|
|
44
56
|
| "status"
|
|
@@ -64,7 +76,6 @@ export interface SdkSessionCliArgs {
|
|
|
64
76
|
jsonInputFile?: string;
|
|
65
77
|
jsonInputStdin?: boolean;
|
|
66
78
|
idempotencyKey?: string;
|
|
67
|
-
scope?: string;
|
|
68
79
|
confirm?: boolean;
|
|
69
80
|
cursor?: string;
|
|
70
81
|
wait?: boolean;
|
|
@@ -73,6 +84,10 @@ export interface SdkSessionCliArgs {
|
|
|
73
84
|
untilIdle?: boolean;
|
|
74
85
|
allEvents?: boolean;
|
|
75
86
|
repo?: string;
|
|
87
|
+
scope?: string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
json?: boolean;
|
|
90
|
+
|
|
76
91
|
agentDir?: string;
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -95,6 +110,9 @@ const TAIL_OFFLINE_MAX_SCAN_BYTES = 4 * 1024 * 1024;
|
|
|
95
110
|
const TAIL_OFFLINE_MAX_SCANNED_LINES = 4_096;
|
|
96
111
|
const TAIL_OFFLINE_MAX_LINE_BYTES = 256 * 1024;
|
|
97
112
|
const transcriptDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
113
|
+
const SEARCH_PROBE_TIMEOUT_MS = 2_000;
|
|
114
|
+
const SEARCH_PROBE_MAX_ROWS = 100;
|
|
115
|
+
const SEARCH_TEXT_WIDTH = 80;
|
|
98
116
|
|
|
99
117
|
const TERMINAL_TURN_KINDS = new Set(["turn_end", "agent_end"]);
|
|
100
118
|
const START_TURN_KINDS = new Set(["turn_start", "agent_start"]);
|
|
@@ -214,6 +232,10 @@ function isEndpointOperation(operation: string): boolean {
|
|
|
214
232
|
return operation === "session.get_endpoint";
|
|
215
233
|
}
|
|
216
234
|
|
|
235
|
+
function isRawSpawnOperation(kind: OperationKind, operation: string): boolean {
|
|
236
|
+
return kind === "global" && operation === "session.spawn";
|
|
237
|
+
}
|
|
238
|
+
|
|
217
239
|
function isLifecycleOperation(operation: string): operation is LifecycleMutationOperation {
|
|
218
240
|
return (
|
|
219
241
|
operation === "session.create" ||
|
|
@@ -257,9 +279,11 @@ async function withRouter<T>(
|
|
|
257
279
|
agentDir: string,
|
|
258
280
|
action: (router: SessionRouter) => Promise<T>,
|
|
259
281
|
onFrame?: (attachment: SessionAttachment, frame: SessionRouterFrame) => void,
|
|
282
|
+
sessionIds?: readonly string[],
|
|
260
283
|
): Promise<T> {
|
|
261
284
|
const router = new SessionRouter({
|
|
262
285
|
agentDir,
|
|
286
|
+
...(sessionIds === undefined ? {} : { sessionIds }),
|
|
263
287
|
...(onFrame === undefined ? {} : { deps: { onFrame } }),
|
|
264
288
|
});
|
|
265
289
|
let result!: T;
|
|
@@ -367,6 +391,145 @@ async function sessionRows(agentDir: string): Promise<SessionRows> {
|
|
|
367
391
|
});
|
|
368
392
|
}
|
|
369
393
|
|
|
394
|
+
function searchScopeRequest(scope: ScopeNameV1, locator: { cwd: string; worktreeRoot: string | null }): ScopeRequestV1 {
|
|
395
|
+
return { version: 1, requested: scope, requestAnchor: { cwd: locator.cwd, worktreeRoot: locator.worktreeRoot } };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function searchProbe(row: SdkSearchRowV1, router: SessionRouter): Promise<SdkSearchRowV1> {
|
|
399
|
+
if (!row.live) return Promise.resolve({ ...row, probe: "stale" });
|
|
400
|
+
return (async () => {
|
|
401
|
+
try {
|
|
402
|
+
const attachment = router.attachment(row.id);
|
|
403
|
+
if (!attachment) return { ...row, probe: "unreachable" };
|
|
404
|
+
const response = await router.request(
|
|
405
|
+
row.id,
|
|
406
|
+
{ type: "query_request", query: "session.checkpoint", input: {} },
|
|
407
|
+
attachment.generation,
|
|
408
|
+
attachment,
|
|
409
|
+
{ timeoutMs: SEARCH_PROBE_TIMEOUT_MS },
|
|
410
|
+
);
|
|
411
|
+
return { ...row, probe: response.ok === true ? "reachable" : "unreachable" };
|
|
412
|
+
} catch {
|
|
413
|
+
return { ...row, probe: "unreachable" };
|
|
414
|
+
}
|
|
415
|
+
})();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function mergeProbedSearchRows(
|
|
419
|
+
rows: readonly SdkSearchRowV1[],
|
|
420
|
+
probedRows: readonly SdkSearchRowV1[],
|
|
421
|
+
): readonly SdkSearchRowV1[] {
|
|
422
|
+
return [...probedRows, ...rows.slice(probedRows.length)];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function probeSearchRows(agentDir: string, result: SdkSearchResultV1): Promise<SdkSearchResultV1> {
|
|
426
|
+
if (result.status === "unavailable" || result.status === "not-in-git-worktree" || result.rows.length === 0)
|
|
427
|
+
return result;
|
|
428
|
+
const rows = result.rows.slice(0, SEARCH_PROBE_MAX_ROWS);
|
|
429
|
+
try {
|
|
430
|
+
const probes = await withRouter(
|
|
431
|
+
agentDir,
|
|
432
|
+
async router => await Promise.all(rows.map(row => searchProbe(row, router))),
|
|
433
|
+
undefined,
|
|
434
|
+
rows.map(row => row.id),
|
|
435
|
+
);
|
|
436
|
+
return { ...result, rows: mergeProbedSearchRows(result.rows, probes) };
|
|
437
|
+
} catch {
|
|
438
|
+
return {
|
|
439
|
+
...result,
|
|
440
|
+
rows: mergeProbedSearchRows(
|
|
441
|
+
result.rows,
|
|
442
|
+
rows.map(row => ({ ...row, probe: row.live ? "unreachable" : "stale" })),
|
|
443
|
+
),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Resolves, lists, and probes only the exact rows selected by the Broker-scoped search. */
|
|
449
|
+
export async function runSdkSearch(
|
|
450
|
+
args: Pick<SdkSessionCliArgs, "agentDir" | "repo" | "scope" | "limit" | "cursor">,
|
|
451
|
+
createService: (agentDir: string) => SessionLifecycleService = createBrokerSessionLifecycleService,
|
|
452
|
+
probe: (agentDir: string, result: SdkSearchResultV1) => Promise<SdkSearchResultV1> = probeSearchRows,
|
|
453
|
+
): Promise<{ result: SdkSearchResultV1; exitCode: 0 | 1 }> {
|
|
454
|
+
const agentDir = args.agentDir ?? getAgentDir();
|
|
455
|
+
const locator = await resolveSessionLocator(args.repo ?? process.cwd(), agentDir);
|
|
456
|
+
const scope: ScopeNameV1 =
|
|
457
|
+
args.scope === undefined || args.scope === "repo" || args.scope === "pwd" || args.scope === "global"
|
|
458
|
+
? ((args.scope ?? "repo") as ScopeNameV1)
|
|
459
|
+
: (() => {
|
|
460
|
+
throw new SdkSessionCliError(
|
|
461
|
+
"usage",
|
|
462
|
+
`Invalid search scope "${args.scope}". Expected repo, pwd, or global.`,
|
|
463
|
+
2,
|
|
464
|
+
);
|
|
465
|
+
})();
|
|
466
|
+
const request = searchScopeRequest(scope, locator);
|
|
467
|
+
const resolved = await resolveScopeRequest(request);
|
|
468
|
+
const outcome = await createService(agentDir).scopedList(request, args.limit, args.cursor);
|
|
469
|
+
if (outcome.ok) {
|
|
470
|
+
if ("rows" in outcome.result) {
|
|
471
|
+
if (outcome.result.status === "not-in-git-worktree") return { result: outcome.result, exitCode: 0 };
|
|
472
|
+
if (outcome.result.status === "unavailable") return { result: outcome.result, exitCode: 1 };
|
|
473
|
+
return { result: await probe(agentDir, outcome.result), exitCode: 0 };
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
result: {
|
|
477
|
+
version: 1,
|
|
478
|
+
scope: resolved,
|
|
479
|
+
status: "unavailable",
|
|
480
|
+
observedAt: new Date().toISOString(),
|
|
481
|
+
rows: [],
|
|
482
|
+
warnings: [],
|
|
483
|
+
error: { code: "malformed_response", message: "broker search returned an unscoped result" },
|
|
484
|
+
},
|
|
485
|
+
exitCode: 1,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (!outcome.ok && outcome.result?.status === "unavailable") return { result: outcome.result, exitCode: 1 };
|
|
489
|
+
return {
|
|
490
|
+
result: {
|
|
491
|
+
version: 1,
|
|
492
|
+
scope: resolved,
|
|
493
|
+
status: "unavailable",
|
|
494
|
+
observedAt: new Date().toISOString(),
|
|
495
|
+
rows: [],
|
|
496
|
+
warnings: [],
|
|
497
|
+
error: { code: outcome.error.code, message: outcome.error.message },
|
|
498
|
+
},
|
|
499
|
+
exitCode: 1,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function searchScopeLabel(result: SdkSearchResultV1): string {
|
|
504
|
+
const resolved = result.scope.resolved;
|
|
505
|
+
if (resolved === null) return "not-in-git-worktree";
|
|
506
|
+
if (resolved.kind === "repo") return resolved.worktreeRoot;
|
|
507
|
+
if (resolved.kind === "pwd") return resolved.cwd;
|
|
508
|
+
return resolved.visibility;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function safeSearchText(value: string): string {
|
|
512
|
+
return truncateToWidth(replaceTabs(value).replaceAll(/[\r\n]/g, " "), SEARCH_TEXT_WIDTH);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Renders a credential-free scope/status preamble and bounded search table. */
|
|
516
|
+
export function renderSdkSearchTable(result: SdkSearchResultV1): string {
|
|
517
|
+
const lines = [
|
|
518
|
+
`Scope requested: ${result.scope.requested}`,
|
|
519
|
+
`Scope resolved: ${safeSearchText(searchScopeLabel(result))}`,
|
|
520
|
+
`Status: ${result.status}`,
|
|
521
|
+
`Observed at: ${result.observedAt}`,
|
|
522
|
+
...(result.cursor === undefined ? [] : [`Continuation cursor: ${safeSearchText(result.cursor)}`]),
|
|
523
|
+
];
|
|
524
|
+
if (result.rows.length === 0) return lines.join("\n");
|
|
525
|
+
lines.push("ID PROBE LIVE CWD");
|
|
526
|
+
for (const row of result.rows)
|
|
527
|
+
lines.push(
|
|
528
|
+
`${safeSearchText(row.id).padEnd(20)} ${(row.probe ?? "-").padEnd(11)} ${String(row.live).padEnd(4)} ${safeSearchText(row.locator.cwd)}`,
|
|
529
|
+
);
|
|
530
|
+
return lines.join("\n");
|
|
531
|
+
}
|
|
532
|
+
|
|
370
533
|
const SESSION_LIST_SCOPES: readonly SdkSessionListScope[] = ["repo", "cwd", "worktree", "all"];
|
|
371
534
|
/** Row workspaces whose locator cannot be canonicalized still get one deterministic identity. */
|
|
372
535
|
const WORKSPACE_IDENTITY_CACHE_LIMIT = 4096;
|
|
@@ -487,7 +650,7 @@ export async function filterSessionRowsByScope(
|
|
|
487
650
|
const warnings: string[] = [];
|
|
488
651
|
const sessions: SdkSessionRowV1[] = [];
|
|
489
652
|
for (const row of rows) {
|
|
490
|
-
const identity = await workspaceIdentity(row.locator.
|
|
653
|
+
const identity = await workspaceIdentity(row.locator.cwd);
|
|
491
654
|
let keep: boolean;
|
|
492
655
|
if (scope === "cwd") keep = identity.canonicalPath === selection.selection.canonicalPath;
|
|
493
656
|
else if (scope === "worktree")
|
|
@@ -496,7 +659,7 @@ export async function filterSessionRowsByScope(
|
|
|
496
659
|
if (keep) sessions.push(row);
|
|
497
660
|
else if (identity.repoRoot === null && identity.commonDir === null)
|
|
498
661
|
warnings.push(
|
|
499
|
-
`Session ${row.sessionId} workspace "${row.locator.
|
|
662
|
+
`Session ${row.sessionId} workspace "${row.locator.cwd}" is outside Git; excluded by scope ${scope}.`,
|
|
500
663
|
);
|
|
501
664
|
}
|
|
502
665
|
return { sessions, warnings };
|
|
@@ -563,7 +726,7 @@ async function requestControl(
|
|
|
563
726
|
const attachment = attachmentFor(router, sessionId);
|
|
564
727
|
const response = await router.request(
|
|
565
728
|
sessionId,
|
|
566
|
-
|
|
729
|
+
controlRequestFrame(operation, input, args),
|
|
567
730
|
attachment.generation,
|
|
568
731
|
attachment,
|
|
569
732
|
args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
|
|
@@ -572,6 +735,81 @@ async function requestControl(
|
|
|
572
735
|
return response;
|
|
573
736
|
}
|
|
574
737
|
|
|
738
|
+
/** Builds the public CLI control envelope without leaking envelope fields into operation input. */
|
|
739
|
+
export function controlRequestFrame(
|
|
740
|
+
operation: string,
|
|
741
|
+
input: JsonRecord,
|
|
742
|
+
args: Pick<SdkSessionCliArgs, "confirm" | "idempotencyKey">,
|
|
743
|
+
): JsonRecord {
|
|
744
|
+
return {
|
|
745
|
+
type: "control_request",
|
|
746
|
+
operation,
|
|
747
|
+
input,
|
|
748
|
+
confirm: args.confirm === true,
|
|
749
|
+
...(args.idempotencyKey ? { idempotencyKey: args.idempotencyKey } : {}),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const BROKER_OPERATOR_ABORT_FIELDS = new Set(["mode", "scope", "operator"]);
|
|
754
|
+
|
|
755
|
+
export function operatorAbortBrokerRequest(
|
|
756
|
+
sessionId: string,
|
|
757
|
+
operation: string,
|
|
758
|
+
input: JsonRecord,
|
|
759
|
+
args: Pick<SdkSessionCliArgs, "confirm" | "idempotencyKey">,
|
|
760
|
+
): JsonRecord | undefined {
|
|
761
|
+
if (!Object.hasOwn(input, "operator")) return undefined;
|
|
762
|
+
if (operation !== "turn.abort")
|
|
763
|
+
throw new SdkSessionCliError("invalid_input", "operator is only available for terminal turn.abort.", 2);
|
|
764
|
+
if (input.operator !== true)
|
|
765
|
+
throw new SdkSessionCliError("invalid_input", "turn.abort operator must be true when provided.", 2);
|
|
766
|
+
if (input.mode !== "terminal")
|
|
767
|
+
throw new SdkSessionCliError("invalid_input", 'operator turn.abort requires mode:"terminal".', 2);
|
|
768
|
+
for (const key of Object.keys(input))
|
|
769
|
+
if (!BROKER_OPERATOR_ABORT_FIELDS.has(key))
|
|
770
|
+
throw new SdkSessionCliError(`invalid_input`, `Unknown turn.abort terminal field: ${key}`, 2);
|
|
771
|
+
if (input.scope !== undefined && input.scope !== "turn" && input.scope !== "owned")
|
|
772
|
+
throw new SdkSessionCliError("invalid_input", 'operator turn.abort scope must be "turn" or "owned".', 2);
|
|
773
|
+
if (args.confirm !== true)
|
|
774
|
+
throw new SdkSessionCliError("invalid_input", "operator terminal abort requires --confirm.", 2);
|
|
775
|
+
if (!args.idempotencyKey)
|
|
776
|
+
throw new SdkSessionCliError("invalid_input", "operator terminal abort requires --idempotency-key.", 2);
|
|
777
|
+
return { sessionId, operation: "turn.abort", input, confirm: true };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function requestBrokerOperatorAbort(
|
|
781
|
+
agentDir: string,
|
|
782
|
+
request: JsonRecord,
|
|
783
|
+
args: SdkSessionCliArgs,
|
|
784
|
+
): Promise<JsonRecord> {
|
|
785
|
+
const idempotencyKey = args.idempotencyKey;
|
|
786
|
+
if (!idempotencyKey)
|
|
787
|
+
throw new SdkSessionCliError("invalid_input", "operator terminal abort requires --idempotency-key.", 2);
|
|
788
|
+
const discovery = await readSdkBrokerDiscovery(agentDir);
|
|
789
|
+
if (!discovery) throw new SdkSessionCliError("session_unavailable", "SDK broker discovery is unavailable.", 1);
|
|
790
|
+
const timeoutMs = args.timeoutMs ?? SESSION_REQUEST_TIMEOUT_MS;
|
|
791
|
+
const client = await SdkClient.connect(discovery.url, discovery.token, {
|
|
792
|
+
timeoutMs,
|
|
793
|
+
reconnectAttempts: 0,
|
|
794
|
+
});
|
|
795
|
+
try {
|
|
796
|
+
const response = await client.global("session.control", request, {
|
|
797
|
+
idempotencyKey,
|
|
798
|
+
timeoutMs,
|
|
799
|
+
});
|
|
800
|
+
const result = object(response);
|
|
801
|
+
if (!result) throw new SdkClientError("protocol_error", "SDK broker returned a malformed control response.");
|
|
802
|
+
throwResponseFailure(result);
|
|
803
|
+
return result;
|
|
804
|
+
} finally {
|
|
805
|
+
await client.close().catch(error => {
|
|
806
|
+
process.stderr.write(
|
|
807
|
+
`SDK broker control cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
575
813
|
async function requestQuery(
|
|
576
814
|
router: SessionRouter,
|
|
577
815
|
sessionId: string,
|
|
@@ -1121,6 +1359,12 @@ async function offlineTailReplay(
|
|
|
1121
1359
|
});
|
|
1122
1360
|
if (!outcome.ok)
|
|
1123
1361
|
throw new SdkSessionCliError(outcome.error.code, outcome.error.message, 1, { certainty: outcome.certainty });
|
|
1362
|
+
if ("rows" in outcome.result)
|
|
1363
|
+
throw new SdkSessionCliError(
|
|
1364
|
+
"malformed_response",
|
|
1365
|
+
"broker returned a scoped result for retained transcript replay",
|
|
1366
|
+
1,
|
|
1367
|
+
);
|
|
1124
1368
|
const savedSession = outcome.result.savedSession;
|
|
1125
1369
|
if (!savedSession)
|
|
1126
1370
|
throw new SdkSessionCliError(
|
|
@@ -1413,6 +1657,8 @@ async function runRawControl(
|
|
|
1413
1657
|
const invalid = validateAdapterControl(operation, input);
|
|
1414
1658
|
if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
|
|
1415
1659
|
await ensureBroker({ agentDir });
|
|
1660
|
+
const operatorRequest = operatorAbortBrokerRequest(sessionId, operation, input, args);
|
|
1661
|
+
if (operatorRequest) return await requestBrokerOperatorAbort(agentDir, operatorRequest, args);
|
|
1416
1662
|
return await withRouter(agentDir, async router => await requestControl(router, sessionId, operation, input, args));
|
|
1417
1663
|
}
|
|
1418
1664
|
|
|
@@ -1506,6 +1752,7 @@ export async function runSdkSessionCli(
|
|
|
1506
1752
|
const action = args.action;
|
|
1507
1753
|
if (
|
|
1508
1754
|
action !== "list" &&
|
|
1755
|
+
action !== "search" &&
|
|
1509
1756
|
action !== "inspect" &&
|
|
1510
1757
|
action !== "send" &&
|
|
1511
1758
|
action !== "status" &&
|
|
@@ -1526,6 +1773,12 @@ export async function runSdkSessionCli(
|
|
|
1526
1773
|
writeOutput(stripSecretFields(await runList(agentDir, args)));
|
|
1527
1774
|
return;
|
|
1528
1775
|
}
|
|
1776
|
+
if (action === "search") {
|
|
1777
|
+
const search = await runSdkSearch(args);
|
|
1778
|
+
writeOutput(args.json === true ? search.result : renderSdkSearchTable(search.result));
|
|
1779
|
+
if (search.exitCode !== 0) setExitCode(search.exitCode);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1529
1782
|
if (action === "inspect") {
|
|
1530
1783
|
writeOutput(stripSecretFields(await runInspect(agentDir, requireValue(args.sessionId, "<sessionId>"))));
|
|
1531
1784
|
return;
|
|
@@ -1577,6 +1830,12 @@ export async function runSdkSessionCli(
|
|
|
1577
1830
|
const kind = rawKind(requireValue(action, "<verb>"), args);
|
|
1578
1831
|
if (!kind) throw new SdkSessionCliError("usage", "raw requires one of: control, query, global.", 2);
|
|
1579
1832
|
const operation = kind === "query" ? requireValue(args.query, "--query") : requireValue(args.operation, "--op");
|
|
1833
|
+
if (isRawSpawnOperation(kind, operation))
|
|
1834
|
+
throw new SdkSessionCliError(
|
|
1835
|
+
"adapter_operation_prohibited",
|
|
1836
|
+
"session.spawn is unavailable through the SDK session CLI.",
|
|
1837
|
+
1,
|
|
1838
|
+
);
|
|
1580
1839
|
const dispositionError = cliOperationError(kind, operation);
|
|
1581
1840
|
if (dispositionError) throw new SdkSessionCliError(dispositionError.code, dispositionError.message, 1);
|
|
1582
1841
|
if (isEndpointOperation(operation))
|
package/src/sdk/client/client.ts
CHANGED
|
@@ -116,13 +116,13 @@ export interface SdkDispatchContext {
|
|
|
116
116
|
readonly generation: number;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/** Request identity retained after an uncertain send
|
|
120
|
-
|
|
119
|
+
/** Request identity retained after an uncertain send. */
|
|
121
120
|
export interface SdkSentRecord {
|
|
122
121
|
readonly id: string;
|
|
123
122
|
readonly operation?: string;
|
|
124
123
|
readonly idempotencyKey?: string;
|
|
125
|
-
|
|
124
|
+
/** Present for lifecycle lookups; `session.spawn` replays through its idempotency key. */
|
|
125
|
+
readonly fingerprint?: string;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
export type SdkFrameHandler = (frame: SdkFrame) => void;
|
|
@@ -473,8 +473,10 @@ export class SdkClient {
|
|
|
473
473
|
const sentOperation = typeof serializedFrame.operation === "string" ? serializedFrame.operation : undefined;
|
|
474
474
|
const sentIdempotencyKey =
|
|
475
475
|
typeof serializedFrame.idempotencyKey === "string" ? serializedFrame.idempotencyKey : undefined;
|
|
476
|
-
const
|
|
477
|
-
|
|
476
|
+
const isSpawn = sentOperation === "session.spawn";
|
|
477
|
+
const sentFingerprint = isSpawn
|
|
478
|
+
? undefined
|
|
479
|
+
: typeof serializedFrame.operation === "string"
|
|
478
480
|
? lifecycleFingerprint(serializedFrame.operation, serializedFrame.input ?? {})
|
|
479
481
|
: inputFingerprint(serializedFrame.input ?? {});
|
|
480
482
|
const deferred = Promise.withResolvers<unknown>();
|
|
@@ -571,11 +573,15 @@ export class SdkClient {
|
|
|
571
573
|
// nothing reached the transport, so the request stays retryable and
|
|
572
574
|
// non-uncertain with no record retained.
|
|
573
575
|
pending.sent = true;
|
|
576
|
+
// A spawn claim replays only through its caller-provided idempotency key;
|
|
577
|
+
// it deliberately has no lifecycle fingerprint. Retain the common sent
|
|
578
|
+
// record anyway so an uncertain transport result tells the caller which
|
|
579
|
+
// key can join that durable claim.
|
|
574
580
|
this.#rememberSentRecord({
|
|
575
581
|
id,
|
|
576
|
-
operation: sentOperation,
|
|
577
|
-
idempotencyKey: sentIdempotencyKey,
|
|
578
|
-
fingerprint: sentFingerprint,
|
|
582
|
+
...(sentOperation === undefined ? {} : { operation: sentOperation }),
|
|
583
|
+
...(sentIdempotencyKey === undefined ? {} : { idempotencyKey: sentIdempotencyKey }),
|
|
584
|
+
...(sentFingerprint === undefined ? {} : { fingerprint: sentFingerprint }),
|
|
579
585
|
});
|
|
580
586
|
try {
|
|
581
587
|
incarnation.socket.send(serializedRequest);
|
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
import { validateRequiredPromptText } from "../../protocol/adapter-validation";
|
|
7
7
|
import { OPERATIONS, type Operation } from "../../protocol/operation-registry";
|
|
8
8
|
import type { ControlInput, ControlSurface, ControlValue } from "./operations";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
|
|
11
|
+
brokerRuntimeCloseCapability,
|
|
12
|
+
hasBrokerRuntimeAbortCapability,
|
|
13
|
+
hasBrokerRuntimeCloseCapability,
|
|
14
|
+
} from "./runtime-gate";
|
|
10
15
|
|
|
11
16
|
export interface ControlRequest {
|
|
12
17
|
id: string;
|
|
@@ -149,29 +154,35 @@ function inputHash(input: unknown): string {
|
|
|
149
154
|
.update(JSON.stringify(canonicalize(input)))
|
|
150
155
|
.digest("hex");
|
|
151
156
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
157
|
+
|
|
158
|
+
export interface TerminalAbortIdentity {
|
|
159
|
+
input: { mode: "terminal"; scope: "turn" | "owned"; operator?: true };
|
|
160
|
+
inputHash: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** One strict, capability-free identity shared by dispatch, durable admission, and delivery observation. */
|
|
164
|
+
export function terminalAbortIdentity(input: unknown, operatorAuthorized: boolean): TerminalAbortIdentity | undefined {
|
|
165
|
+
if (!isInput(input) || input.mode !== "terminal") return undefined;
|
|
166
|
+
for (const key of Object.keys(input)) if (!TERMINAL_ABORT_FIELDS.has(key)) return undefined;
|
|
167
|
+
let scope: "turn" | "owned";
|
|
168
|
+
if (input.scope === undefined || input.scope === "turn") scope = "turn";
|
|
169
|
+
else if (input.scope === "owned") scope = "owned";
|
|
170
|
+
else return undefined;
|
|
171
|
+
if (input.operator !== undefined && input.operator !== true) return undefined;
|
|
172
|
+
if (input.operator === true && !operatorAuthorized) return undefined;
|
|
173
|
+
const normalized = {
|
|
174
|
+
mode: "terminal" as const,
|
|
175
|
+
scope,
|
|
176
|
+
...(input.operator === true ? { operator: true as const } : {}),
|
|
177
|
+
};
|
|
178
|
+
return { input: normalized, inputHash: inputHash(normalized) };
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
function text(input: ControlInput, key = "text"): string {
|
|
171
182
|
return input[key] as string;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
const TERMINAL_ABORT_FIELDS = new Set(["mode", "scope"]);
|
|
185
|
+
const TERMINAL_ABORT_FIELDS = new Set(["mode", "scope", "operator"]);
|
|
175
186
|
|
|
176
187
|
function invalidInput(message: string): never {
|
|
177
188
|
throw new TypedControlError("invalid_input", message);
|
|
@@ -184,15 +195,23 @@ function invalidInput(message: string): never {
|
|
|
184
195
|
* input is dropped and the ordinary argument-less `surface.abort()` runs.
|
|
185
196
|
*
|
|
186
197
|
* Terminal mode (`mode:"terminal"`) is validated strictly and side-effect-free
|
|
187
|
-
* before any surface call: only `mode`/`scope` fields are accepted,
|
|
188
|
-
* must be `"turn"` or `"owned"` (default `"turn"`), and a nonempty
|
|
189
|
-
* key of at most 128 UTF-8 bytes is required on the request envelope.
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
198
|
+
* before any surface call: only `mode`/`scope`/`operator` fields are accepted,
|
|
199
|
+
* `scope` must be `"turn"` or `"owned"` (default `"turn"`), and a nonempty
|
|
200
|
+
* idempotency key of at most 128 UTF-8 bytes is required on the request envelope.
|
|
201
|
+
* A cross-connection local operator must arrive through the Broker route with a
|
|
202
|
+
* private lifecycle capability, set `operator:true`, and confirm the destructive
|
|
203
|
+
* control request explicitly. Terminal semantics (see the approved plan) always
|
|
204
|
+
* stop the root worker's current turn. `scope:"turn"` leaves owned work running
|
|
205
|
+
* so its completion can resume the root worker; `scope:"owned"` additionally
|
|
206
|
+
* requires exact causal proof and stops that owned work. Operator authority
|
|
207
|
+
* changes only connection ownership, never those settlement proofs.
|
|
194
208
|
*/
|
|
195
|
-
function invokeAbort(
|
|
209
|
+
function invokeAbort(
|
|
210
|
+
surface: ControlSurface,
|
|
211
|
+
input: ControlInput,
|
|
212
|
+
confirm: boolean | undefined,
|
|
213
|
+
idempotencyKey: string | undefined,
|
|
214
|
+
): ControlValue {
|
|
196
215
|
const mode = input.mode === undefined ? "turn" : input.mode;
|
|
197
216
|
if (mode === "turn") return surface.abort();
|
|
198
217
|
if (mode !== "terminal") invalidInput('turn.abort mode must be "turn" or "terminal".');
|
|
@@ -200,12 +219,18 @@ function invokeAbort(surface: ControlSurface, input: ControlInput, idempotencyKe
|
|
|
200
219
|
if (!TERMINAL_ABORT_FIELDS.has(key)) invalidInput(`Unknown turn.abort terminal field: ${key}`);
|
|
201
220
|
const scope = input.scope === undefined ? "turn" : input.scope;
|
|
202
221
|
if (scope !== "turn" && scope !== "owned") invalidInput('turn.abort terminal scope must be "turn" or "owned".');
|
|
222
|
+
if (input.operator !== undefined && input.operator !== true)
|
|
223
|
+
invalidInput("turn.abort terminal operator must be true when provided.");
|
|
224
|
+
if (input.operator === true && confirm !== true) invalidInput("operator terminal abort requires confirm:true.");
|
|
203
225
|
if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0)
|
|
204
226
|
invalidInput("terminal abort requires a nonempty idempotency key.");
|
|
205
227
|
if (new TextEncoder().encode(idempotencyKey).length > 128)
|
|
206
228
|
invalidInput("terminal abort idempotency key must be at most 128 UTF-8 bytes.");
|
|
207
229
|
if (!surface.abortTerminal) invalidInput("terminal abort is not supported by this surface.");
|
|
208
|
-
return surface.abortTerminal(
|
|
230
|
+
return surface.abortTerminal(
|
|
231
|
+
{ mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
|
|
232
|
+
idempotencyKey,
|
|
233
|
+
);
|
|
209
234
|
}
|
|
210
235
|
function invoke(
|
|
211
236
|
surface: ControlSurface,
|
|
@@ -222,7 +247,7 @@ function invoke(
|
|
|
222
247
|
case "turn.follow_up":
|
|
223
248
|
return surface.followUp(text(input));
|
|
224
249
|
case "turn.abort":
|
|
225
|
-
return invokeAbort(surface, input, idempotencyKey);
|
|
250
|
+
return invokeAbort(surface, input, confirm, idempotencyKey);
|
|
226
251
|
case "turn.abort_and_prompt":
|
|
227
252
|
return surface.abortAndPrompt(text(input));
|
|
228
253
|
case "ask.answer":
|
|
@@ -404,11 +429,7 @@ function idempotent(
|
|
|
404
429
|
const now = Date.now();
|
|
405
430
|
for (const [key, entry] of requests) if (entry.expiresAt <= now) requests.delete(key);
|
|
406
431
|
const key = `${row.sdkId}\u0000${request.idempotencyKey}`;
|
|
407
|
-
|
|
408
|
-
// defaulted and explicit shapes share one idempotency key (and reach the
|
|
409
|
-
// durable terminal-scope replay on eviction); malformed inputs stay raw.
|
|
410
|
-
const hashInput = row.sdkId === "turn.abort" ? normalizeTerminalAbortInputForHash(request.input) : request.input;
|
|
411
|
-
const hash = inputHash(hashInput);
|
|
432
|
+
const hash = inputHash(request.input);
|
|
412
433
|
const existing = requests.get(key);
|
|
413
434
|
if (existing) {
|
|
414
435
|
requests.delete(key);
|
|
@@ -445,6 +466,10 @@ export function dispatchControl(
|
|
|
445
466
|
failure(request.id, "unknown_operation", `Unknown control operation: ${request.operation}.`),
|
|
446
467
|
);
|
|
447
468
|
const brokerCloseAuthorized = row.sdkId === "session.close" && hasBrokerRuntimeCloseCapability(request.input);
|
|
469
|
+
const brokerAbortInput = row.sdkId === "turn.abort" && isInput(request.input) ? request.input : undefined;
|
|
470
|
+
const brokerAbortFieldPresent =
|
|
471
|
+
brokerAbortInput !== undefined && Object.hasOwn(brokerAbortInput, BROKER_RUNTIME_ABORT_CAPABILITY_FIELD);
|
|
472
|
+
const brokerAbortAuthorized = row.sdkId === "turn.abort" && hasBrokerRuntimeAbortCapability(request.input);
|
|
448
473
|
if (BROKER_LIFECYCLE_CONTROL_OPERATIONS.has(row.sdkId) && !brokerCloseAuthorized)
|
|
449
474
|
return Promise.resolve(
|
|
450
475
|
failure(
|
|
@@ -453,6 +478,20 @@ export function dispatchControl(
|
|
|
453
478
|
`${request.operation} is available only through the Broker lifecycle service.`,
|
|
454
479
|
),
|
|
455
480
|
);
|
|
481
|
+
if (!isInput(request.input))
|
|
482
|
+
return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
|
|
483
|
+
if (
|
|
484
|
+
row.sdkId === "turn.abort" &&
|
|
485
|
+
((brokerAbortFieldPresent && !brokerAbortAuthorized) ||
|
|
486
|
+
(request.input.operator === true && !brokerAbortAuthorized))
|
|
487
|
+
)
|
|
488
|
+
return Promise.resolve(
|
|
489
|
+
failure(
|
|
490
|
+
request.id,
|
|
491
|
+
"operation_prohibited",
|
|
492
|
+
"Operator terminal aborts are available only through the Broker control route.",
|
|
493
|
+
),
|
|
494
|
+
);
|
|
456
495
|
if (
|
|
457
496
|
!brokerCloseAuthorized &&
|
|
458
497
|
surface.installedOperations instanceof Set &&
|
|
@@ -461,20 +500,42 @@ export function dispatchControl(
|
|
|
461
500
|
return Promise.resolve(
|
|
462
501
|
failure(request.id, "operation_not_session_owned", `${request.operation} is not installed for this session.`),
|
|
463
502
|
);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
503
|
+
let dispatchRequest: ControlRequest = request;
|
|
504
|
+
if (row.sdkId === "turn.abort" && request.input.mode === "terminal") {
|
|
505
|
+
const publicInput = brokerAbortInput
|
|
506
|
+
? (() => {
|
|
507
|
+
const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...input } = brokerAbortInput;
|
|
508
|
+
return input;
|
|
509
|
+
})()
|
|
510
|
+
: request.input;
|
|
511
|
+
const identity = terminalAbortIdentity(publicInput, brokerAbortAuthorized);
|
|
512
|
+
if (!identity)
|
|
513
|
+
return Promise.resolve(failure(request.id, "invalid_input", "Terminal turn.abort input is malformed."));
|
|
514
|
+
if (identity.input.operator === true && request.confirm !== true)
|
|
515
|
+
return Promise.resolve(failure(request.id, "invalid_input", "operator terminal abort requires confirm:true."));
|
|
516
|
+
if (typeof request.idempotencyKey !== "string" || request.idempotencyKey.length === 0)
|
|
517
|
+
return Promise.resolve(
|
|
518
|
+
failure(request.id, "invalid_input", "terminal abort requires a nonempty idempotency key."),
|
|
519
|
+
);
|
|
520
|
+
if (new TextEncoder().encode(request.idempotencyKey).length > 128)
|
|
521
|
+
return Promise.resolve(
|
|
522
|
+
failure(request.id, "invalid_input", "terminal abort idempotency key must be at most 128 UTF-8 bytes."),
|
|
523
|
+
);
|
|
524
|
+
dispatchRequest = { ...request, input: identity.input };
|
|
525
|
+
}
|
|
526
|
+
const promptError = validateRequiredPromptText(row.sdkId, dispatchRequest.input as ControlInput);
|
|
467
527
|
if (promptError) return Promise.resolve(failure(request.id, promptError.code, promptError.message));
|
|
468
528
|
if ((row.sdkId === "context.clear" || row.sdkId === "session.delete") && request.confirm !== true)
|
|
469
529
|
return Promise.resolve(
|
|
470
530
|
failure(request.id, "invalid_input", "confirm: true is required for this destructive operation."),
|
|
471
531
|
);
|
|
472
|
-
const work = () => execute(surface, row,
|
|
532
|
+
const work = () => execute(surface, row, dispatchRequest);
|
|
473
533
|
if (row.sdkId === "turn.abort_and_prompt") {
|
|
474
534
|
const cancellable = surface as PreflightCancellableSurface;
|
|
475
535
|
if (Object.hasOwn(cancellable, "cancelPendingPreflights")) cancellable.cancelPendingPreflights?.();
|
|
476
536
|
return serialize(surface, work);
|
|
477
537
|
}
|
|
478
|
-
if (row.idempotency === "idempotent" &&
|
|
538
|
+
if (row.idempotency === "idempotent" && dispatchRequest.idempotencyKey)
|
|
539
|
+
return idempotent(surface, row, dispatchRequest, work);
|
|
479
540
|
return row.idempotency === "ordered" && row.sdkId !== "retry.now" ? serialize(surface, work) : work();
|
|
480
541
|
}
|