@gajae-code/coding-agent 0.18.1 → 0.18.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 +161 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- package/dist/types/cli-commands.d.ts +10 -0
- package/dist/types/cli-ordinary.d.ts +13 -0
- package/dist/types/cli.d.ts +12 -3
- package/dist/types/commands/contribution-prep.d.ts +0 -3
- package/dist/types/commands/sdk.d.ts +3 -0
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.d.ts +2 -2
- package/dist/types/exa/mcp-client.d.ts +1 -5
- package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
- package/dist/types/hashline/parser.d.ts +28 -5
- package/dist/types/modes/acp/acp-agent.d.ts +6 -1
- package/dist/types/sdk/broker/discovery.d.ts +8 -0
- package/dist/types/sdk/broker/ensure.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
- package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
- package/dist/types/sdk/broker/runtime.d.ts +21 -1
- package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
- package/dist/types/sdk/bus/index.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
- package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
- package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +7 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -0
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-main.ts +4 -8
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -186
- package/src/commands/commit.ts +2 -2
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +60 -2
- package/src/commands/update.ts +15 -1
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +73 -5
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +29 -29
- package/src/config/model-registry.ts +45 -17
- package/src/config/model-resolver.ts +9 -1
- package/src/coordinator-mcp/event-webhook.ts +19 -14
- package/src/coordinator-mcp/policy.ts +14 -4
- package/src/coordinator-mcp/server.ts +142 -7
- package/src/coordinator-mcp/session-reaper.ts +9 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +184 -34
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +29 -28
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +688 -371
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/sdk/broker/broker.ts +46 -0
- package/src/sdk/broker/daemon-entry.ts +67 -6
- package/src/sdk/broker/discovery.ts +8 -0
- package/src/sdk/broker/ensure.ts +156 -20
- package/src/sdk/broker/lifecycle-ledger.ts +110 -12
- package/src/sdk/broker/lifecycle.ts +108 -34
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +88 -1
- package/src/sdk/broker/startup-budget.ts +6 -1
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +305 -19
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +15 -12
- package/src/sdk/bus/telegram-daemon.ts +34 -1
- package/src/sdk/cli/diagnostics-cli.ts +247 -0
- package/src/sdk/diagnostics/observe-broker.ts +654 -0
- package/src/sdk/host/control/dispatch.ts +52 -4
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +40 -2
- package/src/sdk/host/session-runtime.ts +112 -8
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +7 -1
- package/src/sdk/session.ts +37 -8
- package/src/session/agent-session.ts +128 -41
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +119 -3
- package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
- package/src/setup/hermes-setup.ts +4 -1
- package/src/slash-commands/builtin-registry.ts +8 -5
- package/src/tools/ask-contract.test.ts +76 -1
- package/src/tools/ask-contract.ts +57 -6
- package/src/tools/ask.ts +16 -7
- package/src/tools/bash.ts +77 -29
- package/src/tools/descriptors.test.ts +29 -0
- package/src/tools/tool-catalog.generated.ts +7 -4
- package/src/web/search/index.ts +2 -2
- package/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/shell.d.ts +0 -21
- package/src/cli/grep-cli.ts +0 -160
- package/src/cli/shell-cli.ts +0 -176
- package/src/commands/grep.ts +0 -48
- package/src/commands/shell.ts +0 -29
package/src/config/file-lock.ts
CHANGED
|
@@ -610,7 +610,7 @@ async function classifyFileLockRemovalTransition(
|
|
|
610
610
|
if (!observation) return "active";
|
|
611
611
|
const info = parseLockInfoBytes(observation.bytes);
|
|
612
612
|
if (info) {
|
|
613
|
-
const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds);
|
|
613
|
+
const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds, undefined, true);
|
|
614
614
|
return stale.stale ? "abandoned" : "active";
|
|
615
615
|
}
|
|
616
616
|
|
|
@@ -698,6 +698,74 @@ async function adoptOrphanedFileLockRemovalTransition(lockPath: string, orphanAg
|
|
|
698
698
|
return false;
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
async function adoptAbandonedFileLockRemovalTransition(
|
|
702
|
+
lockPath: string,
|
|
703
|
+
ownerHostId?: string,
|
|
704
|
+
previousOwnerHostIds: readonly string[] = [],
|
|
705
|
+
): Promise<boolean> {
|
|
706
|
+
const transitionPath = fileLockRemovalTransitionPath(lockPath);
|
|
707
|
+
const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds, undefined, true);
|
|
708
|
+
if (!stale.stale || !stale.identity) return false;
|
|
709
|
+
if (!(await isNativeExactRemovalUsable())) return false;
|
|
710
|
+
|
|
711
|
+
let captured: NativeDirectoryTreeResult;
|
|
712
|
+
try {
|
|
713
|
+
captured = nativeFileLockBindings().snapshotDirectoryTree(transitionPath);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (isTransientReleaseError(error)) return false;
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
if (!captured.ok || !captured.snapshot) return false;
|
|
719
|
+
const snapshot = captured.snapshot;
|
|
720
|
+
const root = snapshot.entries.find(entry => entry.relativePath === "");
|
|
721
|
+
const infoEntry = snapshot.entries.find(entry => entry.relativePath === "info");
|
|
722
|
+
if (
|
|
723
|
+
root?.kind !== "directory" ||
|
|
724
|
+
infoEntry?.kind !== "file" ||
|
|
725
|
+
snapshot.entries.some(
|
|
726
|
+
entry =>
|
|
727
|
+
entry.relativePath !== "" &&
|
|
728
|
+
entry.relativePath !== "info" &&
|
|
729
|
+
entry.kind !== "directory" &&
|
|
730
|
+
(entry.kind !== "file" || entry.size !== "0"),
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
return false;
|
|
734
|
+
const currentIdentity = await captureFileLockDirIdentity(transitionPath);
|
|
735
|
+
if (!currentIdentity || !sameStableFileLockIdentity(currentIdentity, stale.identity)) return false;
|
|
736
|
+
if (
|
|
737
|
+
!nativeFileLockInfoMatchesStableIdentity(snapshot.rootDev, snapshot.rootIno, infoEntry, stale.identity) ||
|
|
738
|
+
!nativeFileLockInfoMatchesContentEvidence(infoEntry, stale.identity)
|
|
739
|
+
)
|
|
740
|
+
return false;
|
|
741
|
+
|
|
742
|
+
let removal: NativeExactUnlinkResult;
|
|
743
|
+
try {
|
|
744
|
+
removal = nativeFileLockBindings().exactRemoveDirectoryTree(transitionPath, snapshot);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (isTransientReleaseError(error)) return false;
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
if (removal.ok === true) return removal.code === undefined && Object.keys(removal).every(key => key === "ok");
|
|
750
|
+
if (
|
|
751
|
+
removal.ok === false &&
|
|
752
|
+
removal.code === "cleanup_pending" &&
|
|
753
|
+
removal.payloadDurable === true &&
|
|
754
|
+
removal.detachedPath !== undefined &&
|
|
755
|
+
path.resolve(removal.detachedPath) === path.resolve(transitionPath) &&
|
|
756
|
+
removal.retainedSuccessorPath === undefined &&
|
|
757
|
+
removal.retainedPlaceholderPath === undefined &&
|
|
758
|
+
removal.retainedUnknownPath === undefined &&
|
|
759
|
+
Object.keys(removal).every(key => ["ok", "code", "payloadDurable", "detachedPath"].includes(key))
|
|
760
|
+
)
|
|
761
|
+
return await removeDetachedLockQuarantineOnDisk(transitionPath, snapshot.rootDev, snapshot.rootIno);
|
|
762
|
+
return (
|
|
763
|
+
removal.ok === false &&
|
|
764
|
+
removal.code === "not_found" &&
|
|
765
|
+
Object.keys(removal).every(key => key === "ok" || key === "code")
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
701
769
|
function sameFileLockTreeAfterPublication(
|
|
702
770
|
staged: NativeDirectoryTreeSnapshot,
|
|
703
771
|
published: NativeDirectoryTreeSnapshot,
|
|
@@ -1488,6 +1556,14 @@ async function staleLockSnapshot(
|
|
|
1488
1556
|
ownerHostId?: string,
|
|
1489
1557
|
previousOwnerHostIds: readonly string[] = [],
|
|
1490
1558
|
startTimeCache?: Map<string, string | null>,
|
|
1559
|
+
// `.removing` transitions are internal cleanup state this codebase's own removal
|
|
1560
|
+
// machinery produced, never a live holder's public identity: a hostless transition
|
|
1561
|
+
// record predates host-id stamping and is reclaimable by local liveness proof alone,
|
|
1562
|
+
// regardless of the acquirer's own host id. Active `<file>.lock` holders keep the
|
|
1563
|
+
// stricter fail-closed rule (see `lockRecordIsForeignHost`) because that record can
|
|
1564
|
+
// belong to a genuinely foreign process; only the two transition-specific callers
|
|
1565
|
+
// below set this.
|
|
1566
|
+
hostlessTransitionIsLocal = false,
|
|
1491
1567
|
): Promise<LockStaleSnapshot> {
|
|
1492
1568
|
// Capture the root and info inode BEFORE asking whether the owner is stale. A later
|
|
1493
1569
|
// snapshot alone would let a copied successor inherit the stale verdict's authority.
|
|
@@ -1521,7 +1597,8 @@ async function staleLockSnapshot(
|
|
|
1521
1597
|
// A host-qualified lock may only be reclaimed after proving that its owner is
|
|
1522
1598
|
// local. Foreign and malformed host-qualified records fail closed: PID values
|
|
1523
1599
|
// and clocks are not meaningful across hosts.
|
|
1524
|
-
|
|
1600
|
+
const foreignCheckHostId = hostlessTransitionIsLocal && info.owner_host_id === undefined ? undefined : ownerHostId;
|
|
1601
|
+
if (lockRecordIsForeignHost(info, foreignCheckHostId, previousOwnerHostIds)) return { stale: false };
|
|
1525
1602
|
if (ownerIncarnationChanged(info, startTimeCache)) {
|
|
1526
1603
|
if (!judgedIdentity) return { stale: false };
|
|
1527
1604
|
let currentIdentity: GenericFileLockDirIdentity | null;
|
|
@@ -1752,6 +1829,18 @@ async function tryAcquireLock(
|
|
|
1752
1829
|
}
|
|
1753
1830
|
if (transitionState === "orphan_transition")
|
|
1754
1831
|
return { kind: "orphan_transition", path: fileLockRemovalTransitionPath(destinationPath) };
|
|
1832
|
+
if (transitionState === "abandoned") {
|
|
1833
|
+
if (await adoptAbandonedFileLockRemovalTransition(destinationPath, ownerHostId, previousOwnerHostIds)) {
|
|
1834
|
+
transitionState = await classifyFileLockRemovalTransition(
|
|
1835
|
+
destinationPath,
|
|
1836
|
+
orphanTransitionAgeMs,
|
|
1837
|
+
ownerHostId,
|
|
1838
|
+
previousOwnerHostIds,
|
|
1839
|
+
);
|
|
1840
|
+
} else {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1755
1844
|
if (transitionState !== null) return null;
|
|
1756
1845
|
const staged = snapshotDirectoryTree(canonicalPendingPath);
|
|
1757
1846
|
if (!staged.ok || !staged.snapshot) {
|
|
@@ -652,6 +652,9 @@ function assertRegistryIdentityPolicy(value: unknown): void {
|
|
|
652
652
|
}
|
|
653
653
|
|
|
654
654
|
function assertCanonicalUnicode(value: string): void {
|
|
655
|
+
// Native well-formedness check first; the scan below only runs to name the
|
|
656
|
+
// offending surrogate when the string is malformed.
|
|
657
|
+
if (value.isWellFormed()) return;
|
|
655
658
|
for (let index = 0; index < value.length; index++) {
|
|
656
659
|
const code = value.charCodeAt(index);
|
|
657
660
|
if (code >= 0xd800 && code <= 0xdbff) {
|
|
@@ -665,6 +668,18 @@ function assertCanonicalUnicode(value: string): void {
|
|
|
665
668
|
}
|
|
666
669
|
}
|
|
667
670
|
|
|
671
|
+
/** Characters JSON.stringify escapes in strings (lone surrogates are rejected earlier). */
|
|
672
|
+
const JSON_STRING_ESCAPES = /["\\\u0000-\u001f]/;
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Quote a string exactly as JSON.stringify does. Registry documents are almost
|
|
676
|
+
* entirely plain ASCII identifiers, so skipping the per-call JSON.stringify for
|
|
677
|
+
* strings with nothing to escape removes most of the serializer's cost.
|
|
678
|
+
*/
|
|
679
|
+
function quoteCanonicalString(value: string): string {
|
|
680
|
+
return JSON_STRING_ESCAPES.test(value) ? JSON.stringify(value) : `"${value}"`;
|
|
681
|
+
}
|
|
682
|
+
|
|
668
683
|
function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
|
|
669
684
|
if (value === null) return "null";
|
|
670
685
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
@@ -674,7 +689,7 @@ function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
|
|
|
674
689
|
}
|
|
675
690
|
if (typeof value === "string") {
|
|
676
691
|
assertCanonicalUnicode(value);
|
|
677
|
-
return
|
|
692
|
+
return quoteCanonicalString(value);
|
|
678
693
|
}
|
|
679
694
|
if (typeof value !== "object") throw new TypeError(`Canonical JSON rejects ${typeof value}.`);
|
|
680
695
|
if (stack.has(value)) throw new TypeError("Canonical JSON rejects cycles.");
|
|
@@ -694,7 +709,7 @@ function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
|
|
|
694
709
|
const entries: string[] = [];
|
|
695
710
|
for (const key of Object.keys(record).sort()) {
|
|
696
711
|
assertCanonicalUnicode(key);
|
|
697
|
-
entries.push(`${
|
|
712
|
+
entries.push(`${quoteCanonicalString(key)}:${serializeCanonicalJson(record[key], stack)}`);
|
|
698
713
|
}
|
|
699
714
|
return `{${entries.join(",")}}`;
|
|
700
715
|
} finally {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
export { resolveModelProfileName } from "./model-profile-contract";
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
|
+
createRegistrySelectorIndex,
|
|
20
21
|
GJC_MODEL_ASSIGNMENT_TARGETS,
|
|
21
22
|
type GjcModelAssignmentTargetId,
|
|
22
23
|
isAuthenticated,
|
|
@@ -114,6 +115,7 @@ export interface PrepareModelProfileActivationOptions {
|
|
|
114
115
|
>
|
|
115
116
|
> & {
|
|
116
117
|
getError?: ModelRegistry["getError"];
|
|
118
|
+
authStorage?: ModelRegistry["authStorage"];
|
|
117
119
|
};
|
|
118
120
|
settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
|
|
119
121
|
profileName: string;
|
|
@@ -474,6 +476,53 @@ export class ModelProfileCredentialError extends Error {
|
|
|
474
476
|
}
|
|
475
477
|
}
|
|
476
478
|
|
|
479
|
+
interface SessionPinRegistry {
|
|
480
|
+
authStorage?: {
|
|
481
|
+
hasRuntimeApiKey(provider: string): boolean;
|
|
482
|
+
hasLiteralConfigApiKey(provider: string, owner?: object): boolean;
|
|
483
|
+
hasSessionCredentialUnavailable(provider: string, scopeId?: string): boolean;
|
|
484
|
+
};
|
|
485
|
+
getAuthStorageOwner?(): object;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Whether an unavailable session credential pin blocks `provider`.
|
|
490
|
+
*
|
|
491
|
+
* Mirrors `AuthStorage.getApiKey` precedence: a runtime `--api-key` override or a literal
|
|
492
|
+
* `models.yml` `apiKey` registered for this registry's owner is resolved before the
|
|
493
|
+
* unavailable-pin marker, so either one keeps the provider usable. An `apiKeyEnv` key does
|
|
494
|
+
* not: `getApiKey` prefers another stored api_key account over it, which would silently
|
|
495
|
+
* retarget the unavailable pin.
|
|
496
|
+
*/
|
|
497
|
+
export function isSessionCredentialPinBlocking(
|
|
498
|
+
registry: SessionPinRegistry,
|
|
499
|
+
provider: string,
|
|
500
|
+
sessionId: string | undefined,
|
|
501
|
+
): boolean {
|
|
502
|
+
const authStorage = registry.authStorage;
|
|
503
|
+
if (!authStorage) return false;
|
|
504
|
+
if (authStorage.hasRuntimeApiKey(provider)) return false;
|
|
505
|
+
if (authStorage.hasLiteralConfigApiKey(provider, registry.getAuthStorageOwner?.())) return false;
|
|
506
|
+
return authStorage.hasSessionCredentialUnavailable(provider, sessionId);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function getProfileProviderApiKey(
|
|
510
|
+
registry: PrepareModelProfileActivationOptions["modelRegistry"],
|
|
511
|
+
provider: string,
|
|
512
|
+
sessionId: string,
|
|
513
|
+
profileLabel: string,
|
|
514
|
+
): Promise<string | undefined> {
|
|
515
|
+
const pinBlocking = () => isSessionCredentialPinBlocking(registry, provider, sessionId);
|
|
516
|
+
if (pinBlocking()) throw new ModelProfileCredentialError(profileLabel, [provider]);
|
|
517
|
+
try {
|
|
518
|
+
return await registry.getApiKeyForProvider(provider, sessionId);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
// OAuth selection can invalidate a pin while the credential probe is running.
|
|
521
|
+
if (pinBlocking()) throw new ModelProfileCredentialError(profileLabel, [provider]);
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
477
526
|
export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
|
|
478
527
|
return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
479
528
|
}
|
|
@@ -675,6 +724,7 @@ export async function resolveMissingSessionModelRecovery(options: {
|
|
|
675
724
|
options.skips.every(skip => skip.reason === "unknown_model");
|
|
676
725
|
if (!allSelectorsUnknown) return undefined;
|
|
677
726
|
const fullCatalog = options.modelRegistry.getAll();
|
|
727
|
+
const catalogIndex = createRegistrySelectorIndex(fullCatalog);
|
|
678
728
|
const savedSelectorsMissingFromCatalog = resolveConfiguredModelPatterns(
|
|
679
729
|
options.defaultEntries,
|
|
680
730
|
options.settings,
|
|
@@ -688,7 +738,7 @@ export async function resolveMissingSessionModelRecovery(options: {
|
|
|
688
738
|
}).model
|
|
689
739
|
)
|
|
690
740
|
return false;
|
|
691
|
-
return !registrySelectorResolvesToModel(selector,
|
|
741
|
+
return !registrySelectorResolvesToModel(selector, catalogIndex);
|
|
692
742
|
});
|
|
693
743
|
const savedConcreteDefault = options.savedDefault ? parseModelString(options.savedDefault) : undefined;
|
|
694
744
|
const savedConcreteDefaultMissingFromCatalog =
|
|
@@ -760,7 +810,12 @@ export async function resolveModelProfileDefaultChain(options: {
|
|
|
760
810
|
])) {
|
|
761
811
|
let apiKey: string | undefined;
|
|
762
812
|
try {
|
|
763
|
-
apiKey = await
|
|
813
|
+
apiKey = await getProfileProviderApiKey(
|
|
814
|
+
options.modelRegistry,
|
|
815
|
+
provider,
|
|
816
|
+
options.credentialSessionId,
|
|
817
|
+
profileLabel,
|
|
818
|
+
);
|
|
764
819
|
} catch (error) {
|
|
765
820
|
if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
|
|
766
821
|
continue;
|
|
@@ -785,7 +840,12 @@ export async function resolveModelProfileDefaultChain(options: {
|
|
|
785
840
|
const proxyApiKey =
|
|
786
841
|
proxyProvider === undefined
|
|
787
842
|
? undefined
|
|
788
|
-
: await
|
|
843
|
+
: await getProfileProviderApiKey(
|
|
844
|
+
options.modelRegistry,
|
|
845
|
+
proxyProvider,
|
|
846
|
+
options.credentialSessionId,
|
|
847
|
+
profileLabel,
|
|
848
|
+
);
|
|
789
849
|
if (
|
|
790
850
|
proxyProvider !== undefined &&
|
|
791
851
|
!isModelProfileProxyConfigured(proxyProvider, configuredProviderIds, proxyApiKey === kNoAuth)
|
|
@@ -884,6 +944,8 @@ export async function resolveModelProfileDefaultChain(options: {
|
|
|
884
944
|
aliasIntent: "preset-equivalent",
|
|
885
945
|
canonicalSessionId: null,
|
|
886
946
|
credentialSessionId: options.credentialSessionId,
|
|
947
|
+
isCredentialUnavailable: provider =>
|
|
948
|
+
isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
|
|
887
949
|
},
|
|
888
950
|
);
|
|
889
951
|
return { profileName, entries: defaultChain, ...resolution };
|
|
@@ -1081,6 +1143,8 @@ async function resolveAndClampSelectorValue(
|
|
|
1081
1143
|
aliasIntent: options.aliasIntent,
|
|
1082
1144
|
canonicalSessionId: options.sessionId,
|
|
1083
1145
|
credentialSessionId: options.credentialSessionId,
|
|
1146
|
+
isCredentialUnavailable: provider =>
|
|
1147
|
+
isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
|
|
1084
1148
|
},
|
|
1085
1149
|
);
|
|
1086
1150
|
resolved = {
|
|
@@ -1211,6 +1275,8 @@ async function concretizeProfileSelectorValue(
|
|
|
1211
1275
|
aliasIntent: "preset-equivalent",
|
|
1212
1276
|
canonicalSessionId: prepared.session.sessionId,
|
|
1213
1277
|
credentialSessionId,
|
|
1278
|
+
isCredentialUnavailable: provider =>
|
|
1279
|
+
isSessionCredentialPinBlocking(prepared.modelRegistry, provider, credentialSessionId),
|
|
1214
1280
|
},
|
|
1215
1281
|
)
|
|
1216
1282
|
: resolveModelRoleValue(selector, candidates, {
|
|
@@ -1320,7 +1386,7 @@ export async function prepareModelProfileActivation(
|
|
|
1320
1386
|
for (const provider of authenticationProbeProviders) {
|
|
1321
1387
|
let apiKey: string | undefined;
|
|
1322
1388
|
try {
|
|
1323
|
-
apiKey = await options.modelRegistry
|
|
1389
|
+
apiKey = await getProfileProviderApiKey(options.modelRegistry, provider, credentialSessionId, profileLabel);
|
|
1324
1390
|
} catch (error) {
|
|
1325
1391
|
if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
|
|
1326
1392
|
continue;
|
|
@@ -1355,7 +1421,7 @@ export async function prepareModelProfileActivation(
|
|
|
1355
1421
|
const proxyApiKey =
|
|
1356
1422
|
proxyProvider === undefined
|
|
1357
1423
|
? undefined
|
|
1358
|
-
: await options.modelRegistry
|
|
1424
|
+
: await getProfileProviderApiKey(options.modelRegistry, proxyProvider, credentialSessionId, profileLabel);
|
|
1359
1425
|
if (proxyProvider !== undefined) {
|
|
1360
1426
|
const configuredProxyProviders = options.modelRegistry.getConfiguredProviderIds?.();
|
|
1361
1427
|
if (!isModelProfileProxyConfigured(proxyProvider, configuredProxyProviders, proxyApiKey === kNoAuth)) {
|
|
@@ -1460,6 +1526,8 @@ export async function prepareModelProfileActivation(
|
|
|
1460
1526
|
aliasIntent: "preset-equivalent",
|
|
1461
1527
|
canonicalSessionId: options.session.sessionId,
|
|
1462
1528
|
credentialSessionId,
|
|
1529
|
+
isCredentialUnavailable: provider =>
|
|
1530
|
+
isSessionCredentialPinBlocking(options.modelRegistry, provider, credentialSessionId),
|
|
1463
1531
|
},
|
|
1464
1532
|
);
|
|
1465
1533
|
const defaultModel = defaultResolution.model;
|
|
@@ -4,7 +4,10 @@ export const MODEL_PROFILE_DISCOVERY_QUERY = "models.profiles.list";
|
|
|
4
4
|
export const MODEL_PROFILE_ERROR_DETAIL_MAX_BYTES = 2048;
|
|
5
5
|
const REQUESTED_PROFILE_MAX_BYTES = 256;
|
|
6
6
|
|
|
7
|
-
const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([
|
|
7
|
+
const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([
|
|
8
|
+
["codex-standard", "codex-medium"],
|
|
9
|
+
["codex-sol61", "codex-pro"],
|
|
10
|
+
]);
|
|
8
11
|
|
|
9
12
|
export interface ModelProfileCatalogItem {
|
|
10
13
|
id: string;
|
|
@@ -73,25 +73,25 @@ const profile = (
|
|
|
73
73
|
|
|
74
74
|
export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
75
75
|
profile("codex-eco", ["openai-codex"], {
|
|
76
|
-
default: "openai-codex/gpt-
|
|
76
|
+
default: "openai-codex/gpt-6-luna:low",
|
|
77
77
|
executor: "openai-codex/gpt-6-luna:low",
|
|
78
78
|
planner: "openai-codex/gpt-6-luna:high",
|
|
79
|
-
critic: "openai-codex/gpt-
|
|
80
|
-
architect: "openai-codex/gpt-
|
|
79
|
+
critic: "openai-codex/gpt-6-luna:xhigh",
|
|
80
|
+
architect: "openai-codex/gpt-6-luna:high",
|
|
81
81
|
}),
|
|
82
82
|
profile("codex-medium", ["openai-codex"], {
|
|
83
|
-
default: "openai-codex/gpt-6-sol:low",
|
|
84
|
-
executor: "openai-codex/gpt-
|
|
85
|
-
planner: "openai-codex/gpt-
|
|
86
|
-
critic: "openai-codex/gpt-6-sol:xhigh",
|
|
87
|
-
architect: "openai-codex/gpt-6-sol:high",
|
|
83
|
+
default: "openai-codex/gpt-6.1-sol:low",
|
|
84
|
+
executor: "openai-codex/gpt-6.1-sol:low",
|
|
85
|
+
planner: "openai-codex/gpt-6.1-sol:high",
|
|
86
|
+
critic: "openai-codex/gpt-6.1-sol:xhigh",
|
|
87
|
+
architect: "openai-codex/gpt-6.1-sol:high",
|
|
88
88
|
}),
|
|
89
89
|
profile("codex-pro", ["openai-codex"], {
|
|
90
|
-
default: "openai-codex/gpt-6-sol:medium",
|
|
91
|
-
executor: "openai-codex/gpt-
|
|
92
|
-
planner: "openai-codex/gpt-6-sol:high",
|
|
93
|
-
critic: "openai-codex/gpt-6-sol:max",
|
|
94
|
-
architect: "openai-codex/gpt-6-sol:xhigh",
|
|
90
|
+
default: "openai-codex/gpt-6.1-sol:medium",
|
|
91
|
+
executor: "openai-codex/gpt-6.1-sol:medium",
|
|
92
|
+
planner: "openai-codex/gpt-6.1-sol:high",
|
|
93
|
+
critic: "openai-codex/gpt-6.1-sol:max",
|
|
94
|
+
architect: "openai-codex/gpt-6.1-sol:xhigh",
|
|
95
95
|
}),
|
|
96
96
|
profile("lunamaxxing", ["openai-codex"], {
|
|
97
97
|
default: "openai-codex/gpt-6-luna:medium",
|
|
@@ -104,20 +104,20 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
104
104
|
default: "openai-codex/gpt-6-astra:medium",
|
|
105
105
|
executor: "openai-codex/gpt-6-luna:xhigh",
|
|
106
106
|
planner: "openai-codex/gpt-6-astra:xhigh",
|
|
107
|
-
critic: "openai-codex/gpt-6-sol:high",
|
|
108
|
-
architect: "openai-codex/gpt-
|
|
107
|
+
critic: "openai-codex/gpt-6.1-sol:high",
|
|
108
|
+
architect: "openai-codex/gpt-6.1-sol:xhigh",
|
|
109
109
|
}),
|
|
110
110
|
profile("astra-default", ["openai-codex"], {
|
|
111
111
|
default: "openai-codex/gpt-6-astra:medium",
|
|
112
112
|
executor: "openai-codex/gpt-6-luna:max",
|
|
113
113
|
planner: "openai-codex/gpt-6-astra:xhigh",
|
|
114
|
-
critic: "openai-codex/gpt-6-sol:xhigh",
|
|
115
|
-
architect: "openai-codex/gpt-
|
|
114
|
+
critic: "openai-codex/gpt-6.1-sol:xhigh",
|
|
115
|
+
architect: "openai-codex/gpt-6.1-sol:xhigh",
|
|
116
116
|
}),
|
|
117
117
|
profile("astra-heavy", ["openai-codex"], {
|
|
118
118
|
default: "openai-codex/gpt-6-astra:medium",
|
|
119
|
-
executor: "openai-codex/gpt-6-sol:max",
|
|
120
|
-
planner: "openai-codex/gpt-6-sol:max",
|
|
119
|
+
executor: "openai-codex/gpt-6.1-sol:max",
|
|
120
|
+
planner: "openai-codex/gpt-6.1-sol:max",
|
|
121
121
|
critic: "openai-codex/gpt-6-astra:max",
|
|
122
122
|
architect: "openai-codex/gpt-6-astra:max",
|
|
123
123
|
}),
|
|
@@ -256,14 +256,14 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
256
256
|
}),
|
|
257
257
|
profile("claude-opus", ["anthropic"], {
|
|
258
258
|
default: ["anthropic/claude-opus-5-5:medium", "anthropic/claude-opus-4-6:xhigh"],
|
|
259
|
-
executor: "anthropic/claude-sonnet-5",
|
|
259
|
+
executor: "anthropic/claude-sonnet-5-5",
|
|
260
260
|
planner: ["anthropic/claude-opus-5-5:medium", "anthropic/claude-opus-4-6:low"],
|
|
261
261
|
critic: ["anthropic/claude-opus-5-5:high", "anthropic/claude-opus-4-6:high"],
|
|
262
262
|
architect: ["anthropic/claude-opus-5-5:high", "anthropic/claude-opus-4-6:xhigh"],
|
|
263
263
|
}),
|
|
264
264
|
profile("claude-fable", ["anthropic"], {
|
|
265
265
|
default: "anthropic/claude-fable-5-1:xhigh",
|
|
266
|
-
executor: "anthropic/claude-sonnet-5",
|
|
266
|
+
executor: "anthropic/claude-sonnet-5-5",
|
|
267
267
|
planner: "anthropic/claude-fable-5-1:low",
|
|
268
268
|
critic: "anthropic/claude-fable-5-1:high",
|
|
269
269
|
architect: "anthropic/claude-fable-5-1:xhigh",
|
|
@@ -490,24 +490,24 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
490
490
|
}),
|
|
491
491
|
profile("opus-codex", ["anthropic", "openai-codex"], {
|
|
492
492
|
default: "anthropic/claude-opus-5-5:medium",
|
|
493
|
-
executor: "openai-codex/gpt-
|
|
494
|
-
planner: "anthropic/claude-sonnet-5",
|
|
495
|
-
critic: "openai-codex/gpt-6-sol:xhigh",
|
|
496
|
-
architect: "openai-codex/gpt-6-sol:high",
|
|
493
|
+
executor: "openai-codex/gpt-6.1-sol:low",
|
|
494
|
+
planner: "anthropic/claude-sonnet-5-5",
|
|
495
|
+
critic: "openai-codex/gpt-6.1-sol:xhigh",
|
|
496
|
+
architect: "openai-codex/gpt-6.1-sol:high",
|
|
497
497
|
}),
|
|
498
498
|
profile("codex-opencodego", ["openai-codex", "opencode-go"], {
|
|
499
|
-
default: "openai-codex/gpt-6-sol:low",
|
|
499
|
+
default: "openai-codex/gpt-6.1-sol:low",
|
|
500
500
|
executor: "opencode-go/deepseek-v4-pro",
|
|
501
501
|
planner: "opencode-go/kimi-k3",
|
|
502
502
|
critic: "opencode-go/mimo-v2.5-pro",
|
|
503
|
-
architect: "openai-codex/gpt-6-sol:high",
|
|
503
|
+
architect: "openai-codex/gpt-6.1-sol:high",
|
|
504
504
|
}),
|
|
505
505
|
profile("fable-opus-codex", ["anthropic", "openai-codex"], {
|
|
506
506
|
default: "anthropic/claude-fable-5-1:high",
|
|
507
|
-
executor: "openai-codex/gpt-
|
|
507
|
+
executor: "openai-codex/gpt-6.1-sol:medium",
|
|
508
508
|
planner: "anthropic/claude-opus-5-5:medium",
|
|
509
509
|
critic: "anthropic/claude-opus-5-5:high",
|
|
510
|
-
architect: "openai-codex/gpt-6-sol:xhigh",
|
|
510
|
+
architect: "openai-codex/gpt-6.1-sol:xhigh",
|
|
511
511
|
}),
|
|
512
512
|
profile("astra-fable", ["openai-codex", "anthropic"], {
|
|
513
513
|
default: "openai-codex/gpt-6-astra:medium",
|
|
@@ -782,26 +782,53 @@ function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Parti
|
|
|
782
782
|
return metadata;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Lowercased lookup keys for a model catalog, so selector resolution is a set
|
|
787
|
+
* lookup instead of a scan that re-lowercases every model id per selector.
|
|
788
|
+
*/
|
|
789
|
+
export interface RegistrySelectorIndex {
|
|
790
|
+
/** `id` and `provider/id`. */
|
|
791
|
+
exact: ReadonlySet<string>;
|
|
792
|
+
/** `id` alone. */
|
|
793
|
+
ids: ReadonlySet<string>;
|
|
794
|
+
/** Model ids per provider, for parsed `provider/id` selectors (nested, so no delimiter can collide). */
|
|
795
|
+
providerIds: ReadonlyMap<string, ReadonlySet<string>>;
|
|
796
|
+
/** Every `/`-delimited suffix of each id (`a/b/c` → `b/c`, `c`). */
|
|
797
|
+
idSuffixes: ReadonlySet<string>;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function createRegistrySelectorIndex(models: readonly Model<Api>[]): RegistrySelectorIndex {
|
|
801
|
+
const exact = new Set<string>();
|
|
802
|
+
const ids = new Set<string>();
|
|
803
|
+
const providerIds = new Map<string, Set<string>>();
|
|
804
|
+
const idSuffixes = new Set<string>();
|
|
805
|
+
for (const model of models) {
|
|
806
|
+
const id = model.id.toLowerCase();
|
|
807
|
+
const provider = model.provider.toLowerCase();
|
|
808
|
+
ids.add(id);
|
|
809
|
+
exact.add(id);
|
|
810
|
+
exact.add(`${provider}/${id}`);
|
|
811
|
+
let providerModelIds = providerIds.get(provider);
|
|
812
|
+
if (!providerModelIds) {
|
|
813
|
+
providerModelIds = new Set<string>();
|
|
814
|
+
providerIds.set(provider, providerModelIds);
|
|
815
|
+
}
|
|
816
|
+
providerModelIds.add(id);
|
|
817
|
+
for (let slash = id.indexOf("/"); slash !== -1; slash = id.indexOf("/", slash + 1)) {
|
|
818
|
+
idSuffixes.add(id.slice(slash + 1));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return { exact, ids, providerIds, idSuffixes };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function registrySelectorResolvesToModel(selector: string, index: RegistrySelectorIndex): boolean {
|
|
786
825
|
const normalizedSelector = selector.trim().toLowerCase();
|
|
787
|
-
if (
|
|
788
|
-
models.some(
|
|
789
|
-
model =>
|
|
790
|
-
model.id.toLowerCase() === normalizedSelector ||
|
|
791
|
-
`${model.provider}/${model.id}`.toLowerCase() === normalizedSelector,
|
|
792
|
-
)
|
|
793
|
-
)
|
|
794
|
-
return true;
|
|
826
|
+
if (index.exact.has(normalizedSelector)) return true;
|
|
795
827
|
const suffix = splitSelectorThinkingSuffix(normalizedSelector);
|
|
796
828
|
const baseSelector = suffix.thinkingLevel === undefined ? normalizedSelector : suffix.selector;
|
|
797
829
|
const parsed = parseModelString(baseSelector);
|
|
798
|
-
if (parsed)
|
|
799
|
-
|
|
800
|
-
model => model.provider.toLowerCase() === parsed.provider && model.id.toLowerCase() === parsed.id,
|
|
801
|
-
);
|
|
802
|
-
return models.some(
|
|
803
|
-
model => model.id.toLowerCase() === baseSelector || model.id.toLowerCase().endsWith(`/${baseSelector}`),
|
|
804
|
-
);
|
|
830
|
+
if (parsed) return index.providerIds.get(parsed.provider)?.has(parsed.id) ?? false;
|
|
831
|
+
return index.ids.has(baseSelector) || index.idSuffixes.has(baseSelector);
|
|
805
832
|
}
|
|
806
833
|
|
|
807
834
|
function filterMaterializedRegistryProfiles(
|
|
@@ -810,6 +837,7 @@ function filterMaterializedRegistryProfiles(
|
|
|
810
837
|
dynamicProviders: ReadonlySet<string>,
|
|
811
838
|
): Map<string, ModelProfileDefinition> {
|
|
812
839
|
const filtered = new Map<string, ModelProfileDefinition>();
|
|
840
|
+
const index = createRegistrySelectorIndex(models);
|
|
813
841
|
for (const [name, profile] of profiles) {
|
|
814
842
|
if (
|
|
815
843
|
profile.source === "registry" &&
|
|
@@ -818,7 +846,7 @@ function filterMaterializedRegistryProfiles(
|
|
|
818
846
|
return (
|
|
819
847
|
selectors.length > 0 &&
|
|
820
848
|
!selectors.some(selector => {
|
|
821
|
-
if (registrySelectorResolvesToModel(selector,
|
|
849
|
+
if (registrySelectorResolvesToModel(selector, index)) return true;
|
|
822
850
|
const suffix = splitSelectorThinkingSuffix(selector);
|
|
823
851
|
const parsed = parseModelString(suffix.thinkingLevel ? suffix.selector : selector);
|
|
824
852
|
return parsed !== undefined && dynamicProviders.has(parsed.provider);
|
|
@@ -1189,7 +1189,15 @@ async function resolveModelChainEntries(
|
|
|
1189
1189
|
skips.push({ selector, reason: "credential_unavailable" });
|
|
1190
1190
|
break;
|
|
1191
1191
|
}
|
|
1192
|
-
|
|
1192
|
+
let key: string | undefined;
|
|
1193
|
+
try {
|
|
1194
|
+
key = await modelRegistry.getApiKey(candidate.model, credentialSessionId, { signal: options?.signal });
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
// OAuth selection can invalidate a pin after the preflight check.
|
|
1197
|
+
if (!options?.isCredentialUnavailable?.(candidate.model.provider)) throw error;
|
|
1198
|
+
skips.push({ selector, reason: "credential_unavailable" });
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1193
1201
|
if (isAuthenticatedOrKeyless(key)) {
|
|
1194
1202
|
return { ...candidate, activeIndex, skips };
|
|
1195
1203
|
}
|
|
@@ -116,6 +116,17 @@ function webhookEnvValue(explicitEnv: NodeJS.ProcessEnv | undefined, name: strin
|
|
|
116
116
|
return $credentialEnv(name)?.trim() || undefined;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Number.parseInt reads a leading digit prefix ("1.5" -> 1, "1e4" -> 1,
|
|
120
|
+
// "10s" -> 10), which would silently turn a timeout into one millisecond or a
|
|
121
|
+
// retry budget into a single attempt. Only plain decimal digits count; anything
|
|
122
|
+
// else falls back to the default, matching the coordinator policy env values.
|
|
123
|
+
function boundedWebhookEnvInt(raw: string | undefined, fallback: number, max: number): number {
|
|
124
|
+
if (raw === undefined || !/^\d+$/.test(raw)) return fallback;
|
|
125
|
+
const parsed = Number.parseInt(raw, 10);
|
|
126
|
+
// An oversized digit string still means "as large as allowed", so it caps.
|
|
127
|
+
return parsed > 0 ? Math.min(parsed, max) : fallback;
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
export function parseEventWebhookConfig(explicitEnv?: NodeJS.ProcessEnv): EventWebhookConfig | null {
|
|
120
131
|
const rawUrl = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_URL");
|
|
121
132
|
if (!rawUrl) return null;
|
|
@@ -139,22 +150,16 @@ export function parseEventWebhookConfig(explicitEnv?: NodeJS.ProcessEnv): EventW
|
|
|
139
150
|
: null;
|
|
140
151
|
const tokenFile = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TOKEN_FILE") ?? "";
|
|
141
152
|
if (tokenFile !== "" && !path.isAbsolute(tokenFile)) throw new Error("coordinator_event_webhook_token_file_invalid");
|
|
142
|
-
const
|
|
143
|
-
webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TIMEOUT_MS")
|
|
144
|
-
|
|
153
|
+
const timeoutMs = boundedWebhookEnvInt(
|
|
154
|
+
webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TIMEOUT_MS"),
|
|
155
|
+
DEFAULT_EVENT_WEBHOOK_TIMEOUT_MS,
|
|
156
|
+
MAX_EVENT_WEBHOOK_TIMEOUT_MS,
|
|
145
157
|
);
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const attemptsRaw = Number.parseInt(
|
|
151
|
-
webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_MAX_ATTEMPTS") ?? "",
|
|
152
|
-
10,
|
|
158
|
+
const maxAttempts = boundedWebhookEnvInt(
|
|
159
|
+
webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_MAX_ATTEMPTS"),
|
|
160
|
+
DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS,
|
|
161
|
+
MAX_EVENT_WEBHOOK_MAX_ATTEMPTS,
|
|
153
162
|
);
|
|
154
|
-
const maxAttempts =
|
|
155
|
-
Number.isFinite(attemptsRaw) && attemptsRaw > 0
|
|
156
|
-
? Math.min(attemptsRaw, MAX_EVENT_WEBHOOK_MAX_ATTEMPTS)
|
|
157
|
-
: DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS;
|
|
158
163
|
return { url: rawUrl, tokenFile: tokenFile || null, sessionIds, timeoutMs, maxAttempts };
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -86,9 +86,19 @@ const LEGACY_MUTATION_CLASS_ALIASES = new Map<string, CoordinatorMutationClass>(
|
|
|
86
86
|
["report", "reports"],
|
|
87
87
|
]);
|
|
88
88
|
|
|
89
|
+
// Number.parseInt reads a leading digit prefix ("64KB" -> 64, "1e6" -> 1,
|
|
90
|
+
// "1.5" -> 1), which would silently shrink a cap or TTL to a tiny value. Only
|
|
91
|
+
// plain decimal digits count; anything else falls back to the default.
|
|
92
|
+
function parsePositiveInt(value: string | undefined): number | undefined {
|
|
93
|
+
const trimmed = (value ?? "").trim();
|
|
94
|
+
if (!/^\d+$/.test(trimmed)) return undefined;
|
|
95
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
96
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
function parsePositiveIntMs(value: string | undefined, fallback: number, floor: number): number {
|
|
90
|
-
const parsed =
|
|
91
|
-
if (
|
|
100
|
+
const parsed = parsePositiveInt(value);
|
|
101
|
+
if (parsed === undefined) return fallback;
|
|
92
102
|
return Math.max(floor, parsed);
|
|
93
103
|
}
|
|
94
104
|
|
|
@@ -133,8 +143,8 @@ function parseMutationClasses(value: string | undefined): Set<CoordinatorMutatio
|
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
function parseByteCap(value: string | undefined): number {
|
|
136
|
-
const parsed =
|
|
137
|
-
if (
|
|
146
|
+
const parsed = parsePositiveInt(value);
|
|
147
|
+
if (parsed === undefined) return DEFAULT_ARTIFACT_BYTE_CAP;
|
|
138
148
|
return Math.min(parsed, MAX_ARTIFACT_BYTE_CAP);
|
|
139
149
|
}
|
|
140
150
|
|