@gajae-code/coding-agent 0.15.4 → 0.15.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
4
5
|
import { getAgentDir } from "@gajae-code/utils";
|
|
6
|
+
import { repo as resolveGitRepository } from "../../utils/git";
|
|
5
7
|
import { ensureBroker } from "../broker/ensure";
|
|
6
8
|
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
7
9
|
import { SdkClientError } from "../client";
|
|
@@ -14,7 +16,11 @@ import type {
|
|
|
14
16
|
SessionReconcileUncertainTarget,
|
|
15
17
|
} from "../lifecycle/service";
|
|
16
18
|
import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
validateAdapterControl,
|
|
21
|
+
validateAdapterSecretFields,
|
|
22
|
+
validateRequiredPromptText,
|
|
23
|
+
} from "../protocol/adapter-validation";
|
|
18
24
|
import { adapterDispositionError, findOperation, type OperationKind } from "../protocol/operation-registry";
|
|
19
25
|
import { type SessionAttachment, SessionRouter, SessionRouterError, type SessionRouterFrame } from "../router";
|
|
20
26
|
import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../session-list";
|
|
@@ -38,10 +44,12 @@ export type SdkSessionCliAction =
|
|
|
38
44
|
| "status"
|
|
39
45
|
| "tail"
|
|
40
46
|
| "retire"
|
|
47
|
+
| "global"
|
|
41
48
|
| "raw"
|
|
42
49
|
| "control"
|
|
43
50
|
| "query"
|
|
44
51
|
| "global";
|
|
52
|
+
export type SdkSessionListScope = "repo" | "cwd" | "worktree" | "all";
|
|
45
53
|
export type SdkSessionCliRawKind = "control" | "query" | "global";
|
|
46
54
|
|
|
47
55
|
export interface SdkSessionCliArgs {
|
|
@@ -56,6 +64,7 @@ export interface SdkSessionCliArgs {
|
|
|
56
64
|
jsonInputFile?: string;
|
|
57
65
|
jsonInputStdin?: boolean;
|
|
58
66
|
idempotencyKey?: string;
|
|
67
|
+
scope?: string;
|
|
59
68
|
confirm?: boolean;
|
|
60
69
|
cursor?: string;
|
|
61
70
|
wait?: boolean;
|
|
@@ -358,16 +367,156 @@ async function sessionRows(agentDir: string): Promise<SessionRows> {
|
|
|
358
367
|
});
|
|
359
368
|
}
|
|
360
369
|
|
|
361
|
-
|
|
370
|
+
const SESSION_LIST_SCOPES: readonly SdkSessionListScope[] = ["repo", "cwd", "worktree", "all"];
|
|
371
|
+
/** Row workspaces whose locator cannot be canonicalized still get one deterministic identity. */
|
|
372
|
+
const WORKSPACE_IDENTITY_CACHE_LIMIT = 4096;
|
|
373
|
+
|
|
374
|
+
interface WorkspaceIdentity {
|
|
375
|
+
/** Canonical workspace path (realpath when it exists, lexical resolve otherwise). */
|
|
376
|
+
canonicalPath: string;
|
|
377
|
+
/** Canonical containing worktree root; null outside a Git checkout. */
|
|
378
|
+
repoRoot: string | null;
|
|
379
|
+
/** Canonical Git common dir; shared by the main checkout and linked worktrees. */
|
|
380
|
+
commonDir: string | null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface SdkSessionListSelection {
|
|
384
|
+
scope: SdkSessionListScope;
|
|
385
|
+
selection: WorkspaceIdentity;
|
|
386
|
+
/** Bounded, credential-free descriptor included in list output. */
|
|
387
|
+
descriptor: {
|
|
388
|
+
scope: SdkSessionListScope;
|
|
389
|
+
path: string;
|
|
390
|
+
worktreeRoot?: string;
|
|
391
|
+
commonDir?: string;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Parses `--scope`; missing means `repo`, anything else invalid is a usage error. */
|
|
396
|
+
export function parseSessionListScope(value: string | undefined): SdkSessionListScope {
|
|
397
|
+
if (value === undefined) return "repo";
|
|
398
|
+
if ((SESSION_LIST_SCOPES as readonly string[]).includes(value)) return value as SdkSessionListScope;
|
|
399
|
+
throw new SdkSessionCliError(
|
|
400
|
+
"usage",
|
|
401
|
+
`Invalid scope "${value}". Expected one of: ${SESSION_LIST_SCOPES.join(", ")}.`,
|
|
402
|
+
2,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function canonicalWorkspacePath(target: string): Promise<string> {
|
|
407
|
+
try {
|
|
408
|
+
return await fs.realpath(target);
|
|
409
|
+
} catch {
|
|
410
|
+
// A removed or unreadable workspace keeps a deterministic lexical identity
|
|
411
|
+
// instead of guessing a broader or narrower one.
|
|
412
|
+
return path.resolve(target);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const workspaceIdentityCache = new Map<string, WorkspaceIdentity>();
|
|
417
|
+
|
|
418
|
+
/** Resolves (and caches) the Git identity of one workspace locator. */
|
|
419
|
+
async function workspaceIdentity(target: string): Promise<WorkspaceIdentity> {
|
|
420
|
+
const canonicalPath = await canonicalWorkspacePath(target);
|
|
421
|
+
const cached = workspaceIdentityCache.get(canonicalPath);
|
|
422
|
+
if (cached) return cached;
|
|
423
|
+
if (target === "unknown") {
|
|
424
|
+
const unavailable = { canonicalPath, repoRoot: null, commonDir: null };
|
|
425
|
+
workspaceIdentityCache.set(canonicalPath, unavailable);
|
|
426
|
+
return unavailable;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
if (!(await fs.stat(target)).isDirectory()) throw new Error("workspace is not a directory");
|
|
430
|
+
} catch {
|
|
431
|
+
// A removed or unreadable locator cannot prove Git membership. Keeping it
|
|
432
|
+
// outside every identity is the narrowest safe result for row filtering.
|
|
433
|
+
const unavailable = { canonicalPath, repoRoot: null, commonDir: null };
|
|
434
|
+
workspaceIdentityCache.set(canonicalPath, unavailable);
|
|
435
|
+
return unavailable;
|
|
436
|
+
}
|
|
437
|
+
const repository = await resolveGitRepository.resolve(canonicalPath);
|
|
438
|
+
const identity: WorkspaceIdentity = repository
|
|
439
|
+
? {
|
|
440
|
+
canonicalPath,
|
|
441
|
+
repoRoot: await canonicalWorkspacePath(repository.repoRoot),
|
|
442
|
+
commonDir: await canonicalWorkspacePath(repository.commonDir),
|
|
443
|
+
}
|
|
444
|
+
: { canonicalPath, repoRoot: null, commonDir: null };
|
|
445
|
+
if (workspaceIdentityCache.size >= WORKSPACE_IDENTITY_CACHE_LIMIT) workspaceIdentityCache.clear();
|
|
446
|
+
workspaceIdentityCache.set(canonicalPath, identity);
|
|
447
|
+
return identity;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Resolves the list selection from `--repo` (default: process cwd) under the
|
|
452
|
+
* effective scope. Outside Git, `repo`/`worktree` fail typed and actionable —
|
|
453
|
+
* they never broaden to `all`; `cwd` remains an exact canonical match.
|
|
454
|
+
*/
|
|
455
|
+
export async function resolveSessionListSelection(
|
|
456
|
+
scope: SdkSessionListScope,
|
|
457
|
+
repoArg: string | undefined,
|
|
458
|
+
): Promise<SdkSessionListSelection> {
|
|
459
|
+
const selection = await workspaceIdentity(repoArg ?? process.cwd());
|
|
460
|
+
if (scope !== "cwd" && !selection.repoRoot)
|
|
461
|
+
throw new SdkSessionCliError(
|
|
462
|
+
"not_a_repository",
|
|
463
|
+
`--scope ${scope} requires a Git repository, but "${selection.canonicalPath}" is outside any Git checkout. ` +
|
|
464
|
+
"Use --scope cwd for an exact workspace match or --scope all for the full broker listing.",
|
|
465
|
+
1,
|
|
466
|
+
);
|
|
467
|
+
const descriptor: SdkSessionListSelection["descriptor"] = {
|
|
468
|
+
scope,
|
|
469
|
+
path: selection.canonicalPath,
|
|
470
|
+
...(selection.repoRoot ? { worktreeRoot: selection.repoRoot, commonDir: selection.commonDir! } : {}),
|
|
471
|
+
};
|
|
472
|
+
return { scope, selection, descriptor };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Filters fully traversed broker rows by the selection scope. Row workspaces
|
|
477
|
+
* are canonicalized and cached per distinct locator; `repo` matches the shared
|
|
478
|
+
* common dir across the main checkout and linked worktrees, `worktree` the
|
|
479
|
+
* containing worktree only, and `cwd` the exact canonical workspace.
|
|
480
|
+
*/
|
|
481
|
+
export async function filterSessionRowsByScope(
|
|
482
|
+
rows: readonly SdkSessionRowV1[],
|
|
483
|
+
scope: SdkSessionListScope,
|
|
484
|
+
selection: SdkSessionListSelection,
|
|
485
|
+
): Promise<{ sessions: SdkSessionRowV1[]; warnings: string[] }> {
|
|
486
|
+
if (scope === "all") return { sessions: [...rows], warnings: [] };
|
|
487
|
+
const warnings: string[] = [];
|
|
488
|
+
const sessions: SdkSessionRowV1[] = [];
|
|
489
|
+
for (const row of rows) {
|
|
490
|
+
const identity = await workspaceIdentity(row.locator.repo);
|
|
491
|
+
let keep: boolean;
|
|
492
|
+
if (scope === "cwd") keep = identity.canonicalPath === selection.selection.canonicalPath;
|
|
493
|
+
else if (scope === "worktree")
|
|
494
|
+
keep = identity.repoRoot !== null && identity.repoRoot === selection.selection.repoRoot;
|
|
495
|
+
else keep = identity.commonDir !== null && identity.commonDir === selection.selection.commonDir;
|
|
496
|
+
if (keep) sessions.push(row);
|
|
497
|
+
else if (identity.repoRoot === null && identity.commonDir === null)
|
|
498
|
+
warnings.push(
|
|
499
|
+
`Session ${row.sessionId} workspace "${row.locator.repo}" is outside Git; excluded by scope ${scope}.`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return { sessions, warnings };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function runList(agentDir: string, args: SdkSessionCliArgs): Promise<unknown> {
|
|
506
|
+
const scope = parseSessionListScope(args.scope);
|
|
507
|
+
const { selection, descriptor } = await resolveSessionListSelection(scope, args.repo);
|
|
362
508
|
const listing = await sessionRows(agentDir);
|
|
509
|
+
const filtered = await filterSessionRowsByScope(listing.sessions, scope, { scope, selection, descriptor });
|
|
363
510
|
return {
|
|
364
511
|
ok: true,
|
|
365
512
|
result: {
|
|
366
513
|
version: SESSION_ROWS_VERSION,
|
|
367
514
|
source: "broker",
|
|
515
|
+
scope,
|
|
516
|
+
selection: descriptor,
|
|
368
517
|
...(listing.indexSeq === undefined ? {} : { indexSeq: listing.indexSeq }),
|
|
369
|
-
sessions:
|
|
370
|
-
warnings: listing.warnings,
|
|
518
|
+
sessions: filtered.sessions,
|
|
519
|
+
warnings: [...listing.warnings, ...filtered.warnings],
|
|
371
520
|
},
|
|
372
521
|
};
|
|
373
522
|
}
|
|
@@ -509,6 +658,8 @@ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliA
|
|
|
509
658
|
if (args.text !== undefined && Object.keys(input).length > 0)
|
|
510
659
|
throw new SdkSessionCliError("usage", "Use either --text or one JSON input source for the prompt, not both.", 2);
|
|
511
660
|
const promptInput: JsonRecord = args.text === undefined ? { ...input } : { text: args.text };
|
|
661
|
+
const promptError = validateRequiredPromptText("turn.prompt", promptInput);
|
|
662
|
+
if (promptError) throw new SdkSessionCliError(promptError.code, promptError.message, 2);
|
|
512
663
|
const inputRef = clientRefFromInput(promptInput);
|
|
513
664
|
const clientRef = inputRef ?? args.opRef?.trim() ?? createOperationRef();
|
|
514
665
|
if (args.opRef !== undefined && inputRef !== undefined && inputRef !== args.opRef.trim())
|
|
@@ -769,6 +920,11 @@ function publicationSequence(publicationId: string): number | undefined {
|
|
|
769
920
|
|
|
770
921
|
function tailItemFromRouterFrame(frame: SessionRouterFrame): SdkTailItemV1 | undefined {
|
|
771
922
|
if (frame.name === undefined) return undefined;
|
|
923
|
+
// The replay response is an out-of-band control frame, not an event-ring
|
|
924
|
+
// item. Its generation/lastSeq metadata is a response position, and it
|
|
925
|
+
// intentionally has no event `seq`; projecting it as a tail item would make
|
|
926
|
+
// the strict event-position validator reject every otherwise valid tail.
|
|
927
|
+
if (frame.name === "event_replay_result") return undefined;
|
|
772
928
|
const seq = frame.publicationId === undefined ? undefined : publicationSequence(frame.publicationId);
|
|
773
929
|
return toTailItemV1(
|
|
774
930
|
{
|
|
@@ -1367,7 +1523,7 @@ export async function runSdkSessionCli(
|
|
|
1367
1523
|
);
|
|
1368
1524
|
const agentDir = args.agentDir ?? getAgentDir();
|
|
1369
1525
|
if (action === "list") {
|
|
1370
|
-
writeOutput(stripSecretFields(await runList(agentDir)));
|
|
1526
|
+
writeOutput(stripSecretFields(await runList(agentDir, args)));
|
|
1371
1527
|
return;
|
|
1372
1528
|
}
|
|
1373
1529
|
if (action === "inspect") {
|
|
@@ -35,7 +35,7 @@ export class SdkDiscoveryError extends Error {
|
|
|
35
35
|
|
|
36
36
|
export type SdkSessionEndpointScope = "default" | "chat";
|
|
37
37
|
|
|
38
|
-
function endpointDirectory(repo: string, scope: SdkSessionEndpointScope = "default"): string {
|
|
38
|
+
export function endpointDirectory(repo: string, scope: SdkSessionEndpointScope = "default"): string {
|
|
39
39
|
return scope === "chat" ? path.join(repo, ".gjc", "state", "chat", "sdk") : path.join(repo, ".gjc", "state", "sdk");
|
|
40
40
|
}
|
|
41
41
|
function isUsableSessionId(sessionId: string): boolean {
|
|
@@ -132,6 +132,8 @@ export async function readSdkSessionEndpoint(
|
|
|
132
132
|
if (!isUsableSessionId(sessionId)) return null;
|
|
133
133
|
const file = path.join(endpointDirectory(repo, scope), `${sessionId}.json`);
|
|
134
134
|
try {
|
|
135
|
+
const stat = await fs.lstat(file);
|
|
136
|
+
if (!stat.isFile()) return null;
|
|
135
137
|
return parseEndpoint(sessionId, file, JSON.parse(await fs.readFile(file, "utf8")));
|
|
136
138
|
} catch (error) {
|
|
137
139
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_MODEL_SELECTION_RECOVERY_MESSAGE,
|
|
4
4
|
parseDefaultModelSelectionRecovery,
|
|
5
5
|
} from "../../../session/default-model-selection";
|
|
6
|
+
import { validateRequiredPromptText } from "../../protocol/adapter-validation";
|
|
6
7
|
import { OPERATIONS, type Operation } from "../../protocol/operation-registry";
|
|
7
8
|
import type { ControlInput, ControlSurface, ControlValue } from "./operations";
|
|
8
9
|
import { brokerRuntimeCloseCapability, hasBrokerRuntimeCloseCapability } from "./runtime-gate";
|
|
@@ -462,6 +463,8 @@ export function dispatchControl(
|
|
|
462
463
|
);
|
|
463
464
|
if (!isInput(request.input))
|
|
464
465
|
return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
|
|
466
|
+
const promptError = validateRequiredPromptText(row.sdkId, request.input);
|
|
467
|
+
if (promptError) return Promise.resolve(failure(request.id, promptError.code, promptError.message));
|
|
465
468
|
if ((row.sdkId === "context.clear" || row.sdkId === "session.delete") && request.confirm !== true)
|
|
466
469
|
return Promise.resolve(
|
|
467
470
|
failure(request.id, "invalid_input", "confirm: true is required for this destructive operation."),
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
|
49
49
|
query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
50
50
|
/** Test/lifecycle seam invoked synchronously before fire-and-forget dispatch. */
|
|
51
51
|
onFrameAdmitted?: (connectionId: string, frame: SdkFrame) => void;
|
|
52
|
+
/** Synchronously rejects external input while a runtime is being replaced. */
|
|
53
|
+
onFrameAdmission?: (connectionId: string, frame: SdkFrame) => SdkFrame | undefined;
|
|
52
54
|
/** Best-effort diagnostic observation of accepted control/query frames. */
|
|
53
55
|
onRequest?: SdkRequestObserver;
|
|
54
56
|
/** Runs before a control response is sent; identity transitions use sendTerminal. */
|
|
@@ -408,6 +410,11 @@ export class SessionSdkHost {
|
|
|
408
410
|
|
|
409
411
|
async #onFrame(connectionId: string, frame: SdkFrame): Promise<void> {
|
|
410
412
|
try {
|
|
413
|
+
const admissionRejection = this.#options.onFrameAdmission?.(connectionId, frame);
|
|
414
|
+
if (admissionRejection !== undefined) {
|
|
415
|
+
await this.#send(connectionId, admissionRejection);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
411
418
|
switch (frame.type) {
|
|
412
419
|
case "control_request": {
|
|
413
420
|
this.#observeRequest("control", connectionId, frame);
|
|
@@ -17,9 +17,19 @@ export interface SdkHostModelResolveContext {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Registry loader owned by the SDK host, not by machine-facing adapters. */
|
|
20
|
-
export type SdkHostModelRegistryLoader = (
|
|
20
|
+
export type SdkHostModelRegistryLoader = ((
|
|
21
21
|
context?: SdkHostModelResolveContext,
|
|
22
|
-
) => ModelRegistry | Promise<ModelRegistry
|
|
22
|
+
) => ModelRegistry | Promise<ModelRegistry>) & {
|
|
23
|
+
acquire?: (context?: SdkHostModelResolveContext) => () => void;
|
|
24
|
+
dispose?: () => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type OwnedModelRegistry = {
|
|
28
|
+
registry: ModelRegistry;
|
|
29
|
+
storage: AuthStorage;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const MAX_CACHED_MODEL_REGISTRIES = 32;
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Builds an offline model resolver for a single SDK host process.
|
|
@@ -32,16 +42,112 @@ export function createSdkHostModelRegistryLoader(
|
|
|
32
42
|
modelsPath?: string,
|
|
33
43
|
loadSettings?: (context?: SdkHostModelResolveContext) => Promise<Pick<Settings, "get" | "getGlobal">>,
|
|
34
44
|
): SdkHostModelRegistryLoader {
|
|
35
|
-
const cachedRegistries = new Map<string, Promise<
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
const cachedRegistries = new Map<string, Promise<OwnedModelRegistry>>();
|
|
46
|
+
const retiredRegistries = new Map<string, Promise<OwnedModelRegistry>[]>();
|
|
47
|
+
const activeScopes = new Map<string, number>();
|
|
48
|
+
const disposedEntries = new WeakSet<Promise<OwnedModelRegistry>>();
|
|
49
|
+
const pendingDisposals = new Set<Promise<void>>();
|
|
50
|
+
const disposalBarrier = Promise.withResolvers<void>();
|
|
51
|
+
const registryAgentDir = path.resolve(path.dirname(modelsPath ?? "."));
|
|
52
|
+
let disposed = false;
|
|
53
|
+
const scopeKeyFor = (context?: SdkHostModelResolveContext): string =>
|
|
54
|
+
context?.cwd === undefined ? "" : path.resolve(context.cwd);
|
|
55
|
+
const disposeEntry = async (entry: Promise<OwnedModelRegistry>): Promise<void> => {
|
|
56
|
+
let owned: OwnedModelRegistry;
|
|
57
|
+
try {
|
|
58
|
+
owned = await entry;
|
|
59
|
+
} catch {
|
|
60
|
+
// Initialization failures close their storage before rejecting. Disposal is
|
|
61
|
+
// best-effort and must not block broker shutdown on one broken scope.
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
owned.registry.dispose();
|
|
66
|
+
} finally {
|
|
67
|
+
owned.storage.close();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const maybeResolveDisposal = (): void => {
|
|
71
|
+
if (
|
|
72
|
+
disposed &&
|
|
73
|
+
cachedRegistries.size === 0 &&
|
|
74
|
+
retiredRegistries.size === 0 &&
|
|
75
|
+
activeScopes.size === 0 &&
|
|
76
|
+
pendingDisposals.size === 0
|
|
77
|
+
)
|
|
78
|
+
disposalBarrier.resolve();
|
|
79
|
+
};
|
|
80
|
+
const disposeEntries = (entries: Promise<OwnedModelRegistry>[]): void => {
|
|
81
|
+
const seen = new Set<Promise<OwnedModelRegistry>>();
|
|
82
|
+
const unique = entries.filter(entry => {
|
|
83
|
+
if (seen.has(entry) || disposedEntries.has(entry)) return false;
|
|
84
|
+
seen.add(entry);
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
if (unique.length === 0) {
|
|
88
|
+
maybeResolveDisposal();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of unique) disposedEntries.add(entry);
|
|
92
|
+
const pending = Promise.all(unique.map(entry => disposeEntry(entry))).then(
|
|
93
|
+
() => undefined,
|
|
94
|
+
() => undefined,
|
|
95
|
+
);
|
|
96
|
+
pendingDisposals.add(pending);
|
|
97
|
+
void pending.finally(() => {
|
|
98
|
+
pendingDisposals.delete(pending);
|
|
99
|
+
maybeResolveDisposal();
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const retireEntry = (scopeKey: string, entry: Promise<OwnedModelRegistry>): void => {
|
|
103
|
+
if ((activeScopes.get(scopeKey) ?? 0) > 0) {
|
|
104
|
+
const retired = retiredRegistries.get(scopeKey) ?? [];
|
|
105
|
+
if (!retired.includes(entry)) retired.push(entry);
|
|
106
|
+
retiredRegistries.set(scopeKey, retired);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
disposeEntries([entry]);
|
|
110
|
+
};
|
|
111
|
+
const disposeRetired = (scopeKey: string): void => {
|
|
112
|
+
const retired = retiredRegistries.get(scopeKey);
|
|
113
|
+
if (!retired || (activeScopes.get(scopeKey) ?? 0) > 0) return;
|
|
114
|
+
retiredRegistries.delete(scopeKey);
|
|
115
|
+
disposeEntries(retired);
|
|
116
|
+
};
|
|
117
|
+
const evictIdleEntries = (): Promise<OwnedModelRegistry>[] => {
|
|
118
|
+
const evicted: Promise<OwnedModelRegistry>[] = [];
|
|
119
|
+
while (cachedRegistries.size > MAX_CACHED_MODEL_REGISTRIES) {
|
|
120
|
+
const oldest = [...cachedRegistries.keys()].find(key => (activeScopes.get(key) ?? 0) === 0);
|
|
121
|
+
if (oldest === undefined) break;
|
|
122
|
+
const entry = cachedRegistries.get(oldest);
|
|
123
|
+
cachedRegistries.delete(oldest);
|
|
124
|
+
if (entry) evicted.push(entry);
|
|
125
|
+
}
|
|
126
|
+
return evicted;
|
|
127
|
+
};
|
|
128
|
+
const loadRegistry = async (context?: SdkHostModelResolveContext): Promise<ModelRegistry> => {
|
|
129
|
+
if (disposed) throw new Error("SDK host model registry loader is disposed.");
|
|
130
|
+
const scopeKey = scopeKeyFor(context);
|
|
38
131
|
let cachedRegistry = cachedRegistries.get(scopeKey);
|
|
39
132
|
if (cachedRegistry === undefined) {
|
|
40
|
-
const initializing =
|
|
41
|
-
|
|
42
|
-
|
|
133
|
+
const initializing = discoverStorage().then(async storage => {
|
|
134
|
+
try {
|
|
135
|
+
const registrySettings = await loadSettings?.(context);
|
|
136
|
+
return {
|
|
137
|
+
storage,
|
|
138
|
+
registry: new ModelRegistry(storage, modelsPath, registrySettings, {
|
|
139
|
+
agentDir: registryAgentDir,
|
|
140
|
+
automaticRefresh: false,
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
storage.close();
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
43
148
|
cachedRegistries.set(scopeKey, initializing);
|
|
44
149
|
cachedRegistry = initializing;
|
|
150
|
+
disposeEntries(evictIdleEntries());
|
|
45
151
|
try {
|
|
46
152
|
await initializing;
|
|
47
153
|
} catch (error) {
|
|
@@ -49,21 +155,67 @@ export function createSdkHostModelRegistryLoader(
|
|
|
49
155
|
throw error;
|
|
50
156
|
}
|
|
51
157
|
}
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
158
|
+
const owned = await cachedRegistry;
|
|
159
|
+
if (disposed || disposedEntries.has(cachedRegistry) || cachedRegistries.get(scopeKey) !== cachedRegistry) {
|
|
160
|
+
retireEntry(scopeKey, cachedRegistry);
|
|
161
|
+
throw new Error("SDK host model registry loader is disposed.");
|
|
162
|
+
}
|
|
163
|
+
cachedRegistries.delete(scopeKey);
|
|
164
|
+
cachedRegistries.set(scopeKey, cachedRegistry);
|
|
165
|
+
const registry = owned.registry;
|
|
166
|
+
if (loadSettings) {
|
|
167
|
+
const registrySettings = await loadSettings(context);
|
|
168
|
+
if (disposed || disposedEntries.has(cachedRegistry)) {
|
|
169
|
+
retireEntry(scopeKey, cachedRegistry);
|
|
170
|
+
throw new Error("SDK host model registry loader is disposed.");
|
|
171
|
+
}
|
|
172
|
+
registry.setScopedSettings(registrySettings);
|
|
173
|
+
}
|
|
54
174
|
await registry.refresh("offline");
|
|
175
|
+
if (disposed || disposedEntries.has(cachedRegistry)) {
|
|
176
|
+
retireEntry(scopeKey, cachedRegistry);
|
|
177
|
+
throw new Error("SDK host model registry loader is disposed.");
|
|
178
|
+
}
|
|
55
179
|
return registry;
|
|
56
180
|
};
|
|
181
|
+
return Object.assign(loadRegistry, {
|
|
182
|
+
dispose: async (): Promise<void> => {
|
|
183
|
+
if (disposed) return await disposalBarrier.promise;
|
|
184
|
+
disposed = true;
|
|
185
|
+
for (const [scopeKey, entry] of cachedRegistries) retireEntry(scopeKey, entry);
|
|
186
|
+
cachedRegistries.clear();
|
|
187
|
+
for (const scopeKey of retiredRegistries.keys()) disposeRetired(scopeKey);
|
|
188
|
+
maybeResolveDisposal();
|
|
189
|
+
await disposalBarrier.promise;
|
|
190
|
+
},
|
|
191
|
+
acquire: (context?: SdkHostModelResolveContext): (() => void) => {
|
|
192
|
+
const scopeKey = scopeKeyFor(context);
|
|
193
|
+
activeScopes.set(scopeKey, (activeScopes.get(scopeKey) ?? 0) + 1);
|
|
194
|
+
let released = false;
|
|
195
|
+
return () => {
|
|
196
|
+
if (released) return;
|
|
197
|
+
released = true;
|
|
198
|
+
const next = (activeScopes.get(scopeKey) ?? 1) - 1;
|
|
199
|
+
if (next <= 0) activeScopes.delete(scopeKey);
|
|
200
|
+
else activeScopes.set(scopeKey, next);
|
|
201
|
+
disposeRetired(scopeKey);
|
|
202
|
+
disposeEntries(evictIdleEntries());
|
|
203
|
+
maybeResolveDisposal();
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
});
|
|
57
207
|
}
|
|
58
208
|
|
|
59
209
|
export type SdkHostModelResolution =
|
|
60
210
|
| { ok: true; model: string | null }
|
|
61
211
|
| { ok: false; reason: "unknown_model"; model: string; error: string };
|
|
62
212
|
|
|
63
|
-
export type SdkHostModelResolver = (
|
|
213
|
+
export type SdkHostModelResolver = ((
|
|
64
214
|
raw: unknown,
|
|
65
215
|
context?: SdkHostModelResolveContext,
|
|
66
|
-
) => Promise<SdkHostModelResolution
|
|
216
|
+
) => Promise<SdkHostModelResolution>) & {
|
|
217
|
+
dispose?: () => Promise<void>;
|
|
218
|
+
};
|
|
67
219
|
|
|
68
220
|
/** Resolve the explicit model pin at the SDK host boundary. */
|
|
69
221
|
export async function resolveSdkHostModel(
|
|
@@ -74,19 +226,24 @@ export async function resolveSdkHostModel(
|
|
|
74
226
|
if (raw === undefined || raw === null) return { ok: true, model: null };
|
|
75
227
|
const requested = typeof raw === "string" ? raw : "";
|
|
76
228
|
const echoed = requested.trim().slice(0, MAX_ECHOED_MODEL_LENGTH);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
229
|
+
const release = loadRegistry.acquire?.(context) ?? (() => {});
|
|
230
|
+
try {
|
|
231
|
+
const registry = await loadRegistry(context);
|
|
232
|
+
const resolved: ResolveCliModelResult = resolveCliModel({ cliModel: requested, modelRegistry: registry });
|
|
233
|
+
if (!resolved.model)
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
reason: "unknown_model",
|
|
237
|
+
model: echoed,
|
|
238
|
+
error: resolved.error ?? "No models available. Check your installation or add models to models.json.",
|
|
239
|
+
};
|
|
80
240
|
return {
|
|
81
|
-
ok:
|
|
82
|
-
|
|
83
|
-
model: echoed,
|
|
84
|
-
error: resolved.error ?? "No models available. Check your installation or add models to models.json.",
|
|
241
|
+
ok: true,
|
|
242
|
+
model: formatModelSelectorValue(formatModelString(resolved.model), resolved.thinkingLevel),
|
|
85
243
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
244
|
+
} finally {
|
|
245
|
+
release();
|
|
246
|
+
}
|
|
90
247
|
}
|
|
91
248
|
|
|
92
249
|
/** Default resolver used by the SDK broker host. */
|
|
@@ -96,6 +253,9 @@ export function createDefaultSdkHostModelResolver(agentDir: string): SdkHostMode
|
|
|
96
253
|
path.join(agentDir, "models.yml"),
|
|
97
254
|
context => Settings.loadReadonly({ agentDir, cwd: context?.cwd }),
|
|
98
255
|
);
|
|
99
|
-
return (
|
|
100
|
-
|
|
256
|
+
return Object.assign(
|
|
257
|
+
(raw: unknown, context?: SdkHostModelResolveContext): Promise<SdkHostModelResolution> =>
|
|
258
|
+
resolveSdkHostModel(raw, loadRegistry, context),
|
|
259
|
+
{ dispose: loadRegistry.dispose },
|
|
260
|
+
);
|
|
101
261
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSdkRunCapability } from "../../session/sdk-run-capability-internal";
|