@rk0429/agentic-relay 1.2.0 → 1.3.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/dist/relay.mjs +212 -305
- package/package.json +1 -1
package/dist/relay.mjs
CHANGED
|
@@ -46,6 +46,120 @@ var init_logger = __esm({
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// src/core/metadata-validation.ts
|
|
50
|
+
function isPlainObject(value) {
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const prototype = Object.getPrototypeOf(value);
|
|
55
|
+
return prototype === Object.prototype || prototype === null;
|
|
56
|
+
}
|
|
57
|
+
function utf8Size(value) {
|
|
58
|
+
return new TextEncoder().encode(value).length;
|
|
59
|
+
}
|
|
60
|
+
function validateMetadataKey(key) {
|
|
61
|
+
if (DANGEROUS_METADATA_KEYS.has(key)) {
|
|
62
|
+
throw new Error(`metadata key "${key}" is not allowed`);
|
|
63
|
+
}
|
|
64
|
+
if (key.length > MAX_METADATA_KEY_LENGTH) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`metadata key "${key}" exceeds ${MAX_METADATA_KEY_LENGTH} chars`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function validateMetadataValue(value, path2, depth) {
|
|
71
|
+
if (depth > MAX_METADATA_NESTING_DEPTH) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`metadata nesting depth exceeds ${MAX_METADATA_NESTING_DEPTH} at "${path2}"`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "string") {
|
|
77
|
+
if (value.length > MAX_METADATA_STRING_VALUE_LENGTH) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`metadata string at "${path2}" exceeds ${MAX_METADATA_STRING_VALUE_LENGTH} chars`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean") {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
89
|
+
validateMetadataValue(value[index], `${path2}[${index}]`, depth + 1);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === "object") {
|
|
94
|
+
if (!isPlainObject(value)) {
|
|
95
|
+
throw new Error(`metadata value at "${path2}" must be a plain object`);
|
|
96
|
+
}
|
|
97
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
98
|
+
validateMetadataKey(key);
|
|
99
|
+
validateMetadataValue(nestedValue, `${path2}.${key}`, depth + 1);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`metadata value at "${path2}" has unsupported type`);
|
|
104
|
+
}
|
|
105
|
+
function validateMetadata(raw) {
|
|
106
|
+
if (raw === void 0 || raw === null) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
if (!isPlainObject(raw)) {
|
|
110
|
+
throw new Error("metadata must be a plain object");
|
|
111
|
+
}
|
|
112
|
+
let serialized;
|
|
113
|
+
try {
|
|
114
|
+
serialized = JSON.stringify(raw);
|
|
115
|
+
} catch {
|
|
116
|
+
throw new Error("metadata must be JSON-serializable");
|
|
117
|
+
}
|
|
118
|
+
if (utf8Size(serialized) > MAX_METADATA_SIZE_BYTES) {
|
|
119
|
+
throw new Error(`metadata exceeds ${MAX_METADATA_SIZE_BYTES} bytes`);
|
|
120
|
+
}
|
|
121
|
+
const entries = Object.entries(raw);
|
|
122
|
+
if (entries.length > MAX_METADATA_KEY_COUNT) {
|
|
123
|
+
throw new Error(`metadata has ${entries.length} keys, max is ${MAX_METADATA_KEY_COUNT}`);
|
|
124
|
+
}
|
|
125
|
+
for (const [key, value] of entries) {
|
|
126
|
+
validateMetadataKey(key);
|
|
127
|
+
validateMetadataValue(value, key, 1);
|
|
128
|
+
}
|
|
129
|
+
const typed = raw;
|
|
130
|
+
if (typed.taskId !== void 0 && typeof typed.taskId !== "string") {
|
|
131
|
+
throw new Error("metadata.taskId must be a string");
|
|
132
|
+
}
|
|
133
|
+
if (typed.taskId !== void 0 && typeof typed.taskId === "string" && !TASK_ID_PATTERN.test(typed.taskId)) {
|
|
134
|
+
throw new Error("metadata.taskId must match ^(TASK|GOAL)-\\d{3,}$");
|
|
135
|
+
}
|
|
136
|
+
if (typed.agentType !== void 0 && typeof typed.agentType !== "string") {
|
|
137
|
+
throw new Error("metadata.agentType must be a string");
|
|
138
|
+
}
|
|
139
|
+
if (typed.label !== void 0 && typeof typed.label !== "string") {
|
|
140
|
+
throw new Error("metadata.label must be a string");
|
|
141
|
+
}
|
|
142
|
+
if (typed.tags !== void 0) {
|
|
143
|
+
if (!Array.isArray(typed.tags) || !typed.tags.every((tag) => typeof tag === "string")) {
|
|
144
|
+
throw new Error("metadata.tags must be string[]");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return typed;
|
|
148
|
+
}
|
|
149
|
+
var DANGEROUS_METADATA_KEYS, TASK_ID_PATTERN, MAX_METADATA_SIZE_BYTES, MAX_METADATA_KEY_COUNT, MAX_METADATA_KEY_LENGTH, MAX_METADATA_STRING_VALUE_LENGTH, MAX_METADATA_NESTING_DEPTH;
|
|
150
|
+
var init_metadata_validation = __esm({
|
|
151
|
+
"src/core/metadata-validation.ts"() {
|
|
152
|
+
"use strict";
|
|
153
|
+
DANGEROUS_METADATA_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
154
|
+
TASK_ID_PATTERN = /^(TASK|GOAL)-\d{3,}$/;
|
|
155
|
+
MAX_METADATA_SIZE_BYTES = 8 * 1024;
|
|
156
|
+
MAX_METADATA_KEY_COUNT = 20;
|
|
157
|
+
MAX_METADATA_KEY_LENGTH = 64;
|
|
158
|
+
MAX_METADATA_STRING_VALUE_LENGTH = 1024;
|
|
159
|
+
MAX_METADATA_NESTING_DEPTH = 3;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
49
163
|
// src/mcp-server/deferred-cleanup-task-store.ts
|
|
50
164
|
import { isTerminal } from "@modelcontextprotocol/sdk/experimental/tasks/interfaces.js";
|
|
51
165
|
import { randomBytes } from "crypto";
|
|
@@ -856,116 +970,16 @@ function buildChildMcpServers(parentMcpServers, childHttpUrl) {
|
|
|
856
970
|
}
|
|
857
971
|
return result;
|
|
858
972
|
}
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
return prototype === Object.prototype || prototype === null;
|
|
865
|
-
}
|
|
866
|
-
function utf8Size(value) {
|
|
867
|
-
return new TextEncoder().encode(value).length;
|
|
868
|
-
}
|
|
869
|
-
function validateMetadataKey(key) {
|
|
870
|
-
if (DANGEROUS_METADATA_KEYS.has(key)) {
|
|
871
|
-
throw new Error(`metadata key "${key}" is not allowed`);
|
|
872
|
-
}
|
|
873
|
-
if (key.length > MAX_METADATA_KEY_LENGTH) {
|
|
874
|
-
throw new Error(
|
|
875
|
-
`metadata key "${key}" exceeds ${MAX_METADATA_KEY_LENGTH} chars`
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
function validateMetadataValue(value, path2, depth) {
|
|
880
|
-
if (depth > MAX_METADATA_NESTING_DEPTH) {
|
|
881
|
-
throw new Error(
|
|
882
|
-
`metadata nesting depth exceeds ${MAX_METADATA_NESTING_DEPTH} at "${path2}"`
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
if (typeof value === "string") {
|
|
886
|
-
if (value.length > MAX_METADATA_STRING_VALUE_LENGTH) {
|
|
887
|
-
throw new Error(
|
|
888
|
-
`metadata string at "${path2}" exceeds ${MAX_METADATA_STRING_VALUE_LENGTH} chars`
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean") {
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
if (Array.isArray(value)) {
|
|
897
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
898
|
-
validateMetadataValue(value[index], `${path2}[${index}]`, depth + 1);
|
|
899
|
-
}
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (typeof value === "object") {
|
|
903
|
-
if (!isPlainObject2(value)) {
|
|
904
|
-
throw new Error(`metadata value at "${path2}" must be a plain object`);
|
|
905
|
-
}
|
|
906
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
907
|
-
validateMetadataKey(key);
|
|
908
|
-
validateMetadataValue(nestedValue, `${path2}.${key}`, depth + 1);
|
|
909
|
-
}
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
throw new Error(`metadata value at "${path2}" has unsupported type`);
|
|
913
|
-
}
|
|
914
|
-
function validateMetadata(raw) {
|
|
915
|
-
if (raw === void 0 || raw === null) {
|
|
916
|
-
return {};
|
|
917
|
-
}
|
|
918
|
-
if (!isPlainObject2(raw)) {
|
|
919
|
-
throw new Error("metadata must be a plain object");
|
|
920
|
-
}
|
|
921
|
-
let serialized;
|
|
922
|
-
try {
|
|
923
|
-
serialized = JSON.stringify(raw);
|
|
924
|
-
} catch {
|
|
925
|
-
throw new Error("metadata must be JSON-serializable");
|
|
973
|
+
function resolveValidatedSessionMetadata(input) {
|
|
974
|
+
const validated = validateMetadata(input.metadata);
|
|
975
|
+
const mergedMetadata = { ...validated };
|
|
976
|
+
if (mergedMetadata.agentType === void 0 && input.agent !== void 0) {
|
|
977
|
+
mergedMetadata.agentType = input.agent;
|
|
926
978
|
}
|
|
927
|
-
if (
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
const entries = Object.entries(raw);
|
|
931
|
-
if (entries.length > MAX_METADATA_KEY_COUNT) {
|
|
932
|
-
throw new Error(`metadata has ${entries.length} keys, max is ${MAX_METADATA_KEY_COUNT}`);
|
|
933
|
-
}
|
|
934
|
-
for (const [key, value] of entries) {
|
|
935
|
-
validateMetadataKey(key);
|
|
936
|
-
validateMetadataValue(value, key, 1);
|
|
937
|
-
}
|
|
938
|
-
const typed = raw;
|
|
939
|
-
if (typed.taskId !== void 0 && typeof typed.taskId !== "string") {
|
|
940
|
-
throw new Error("metadata.taskId must be a string");
|
|
979
|
+
if (mergedMetadata.label === void 0 && input.label !== void 0) {
|
|
980
|
+
mergedMetadata.label = input.label;
|
|
941
981
|
}
|
|
942
|
-
|
|
943
|
-
throw new Error("metadata.taskId must match ^(TASK|GOAL)-\\d{3,}$");
|
|
944
|
-
}
|
|
945
|
-
if (typed.agentType !== void 0 && typeof typed.agentType !== "string") {
|
|
946
|
-
throw new Error("metadata.agentType must be a string");
|
|
947
|
-
}
|
|
948
|
-
if (typed.label !== void 0 && typeof typed.label !== "string") {
|
|
949
|
-
throw new Error("metadata.label must be a string");
|
|
950
|
-
}
|
|
951
|
-
if (typed.tags !== void 0) {
|
|
952
|
-
if (!Array.isArray(typed.tags) || !typed.tags.every((tag) => typeof tag === "string")) {
|
|
953
|
-
throw new Error("metadata.tags must be string[]");
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
return typed;
|
|
957
|
-
}
|
|
958
|
-
function getLastActivityAtMs(session) {
|
|
959
|
-
return Math.max(
|
|
960
|
-
session.lastHeartbeatAt.getTime(),
|
|
961
|
-
session.updatedAt.getTime()
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
function isActiveSessionStale(session, staleThresholdMs, now) {
|
|
965
|
-
if (session.status !== "active") {
|
|
966
|
-
return false;
|
|
967
|
-
}
|
|
968
|
-
return getLastActivityAtMs(session) + staleThresholdMs < now;
|
|
982
|
+
return validateMetadata(mergedMetadata);
|
|
969
983
|
}
|
|
970
984
|
function inferFailureReason(stderr, stdout, sdkErrorMetadata) {
|
|
971
985
|
if (sdkErrorMetadata) {
|
|
@@ -984,10 +998,6 @@ function failureReasonToErrorCode(reason) {
|
|
|
984
998
|
return "RELAY_RECURSION_BLOCKED";
|
|
985
999
|
case "metadata_validation":
|
|
986
1000
|
return "RELAY_METADATA_VALIDATION";
|
|
987
|
-
case "max_sessions_exceeded":
|
|
988
|
-
return "RELAY_MAX_SESSIONS_EXCEEDED";
|
|
989
|
-
case "concurrent_limit_race":
|
|
990
|
-
return "RELAY_CONCURRENT_LIMIT_RACE";
|
|
991
1001
|
case "backend_unavailable":
|
|
992
1002
|
return "RELAY_BACKEND_UNAVAILABLE";
|
|
993
1003
|
case "instruction_file_error":
|
|
@@ -1017,7 +1027,7 @@ function buildContextFromEnv() {
|
|
|
1017
1027
|
const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
|
|
1018
1028
|
return { traceId, parentSessionId, depth };
|
|
1019
1029
|
}
|
|
1020
|
-
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory"
|
|
1030
|
+
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory") {
|
|
1021
1031
|
onProgress?.({ stage: "initializing", percent: 0 });
|
|
1022
1032
|
let effectiveBackend;
|
|
1023
1033
|
let selectionReason;
|
|
@@ -1073,15 +1083,7 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
1073
1083
|
const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1074
1084
|
let validatedMetadata;
|
|
1075
1085
|
try {
|
|
1076
|
-
validatedMetadata =
|
|
1077
|
-
const mergedMetadata = { ...validatedMetadata };
|
|
1078
|
-
if (mergedMetadata.agentType === void 0 && input.agent !== void 0) {
|
|
1079
|
-
mergedMetadata.agentType = input.agent;
|
|
1080
|
-
}
|
|
1081
|
-
if (mergedMetadata.label === void 0 && input.label !== void 0) {
|
|
1082
|
-
mergedMetadata.label = input.label;
|
|
1083
|
-
}
|
|
1084
|
-
validatedMetadata = validateMetadata(mergedMetadata);
|
|
1086
|
+
validatedMetadata = resolveValidatedSessionMetadata(input);
|
|
1085
1087
|
} catch (error) {
|
|
1086
1088
|
const message = error instanceof Error ? error.message : String(error);
|
|
1087
1089
|
return {
|
|
@@ -1092,79 +1094,22 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
1092
1094
|
failureReason: "metadata_validation"
|
|
1093
1095
|
};
|
|
1094
1096
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
let session = null;
|
|
1106
|
-
let lastCreateError = "";
|
|
1107
|
-
for (let attempt = 0; attempt < maxCreateAttempts; attempt += 1) {
|
|
1108
|
-
const activeSessions = await sessionManager2.list({ status: "active" });
|
|
1109
|
-
const now = Date.now();
|
|
1110
|
-
const activeHealthyCount = activeSessions.filter(
|
|
1111
|
-
(activeSession) => !isActiveSessionStale(activeSession, staleThresholdMs, now)
|
|
1112
|
-
).length;
|
|
1113
|
-
if (activeHealthyCount >= maxActiveSessions) {
|
|
1114
|
-
return {
|
|
1115
|
-
sessionId: "",
|
|
1116
|
-
exitCode: 1,
|
|
1117
|
-
stdout: "",
|
|
1118
|
-
stderr: `RELAY_MAX_SESSIONS_EXCEEDED: active sessions ${activeHealthyCount}/${maxActiveSessions}`,
|
|
1119
|
-
failureReason: "max_sessions_exceeded"
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
if (activeHealthyCount >= warnThreshold) {
|
|
1123
|
-
logger.warn(
|
|
1124
|
-
`Active session usage high during spawn: ${activeHealthyCount}/${maxActiveSessions}`
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
try {
|
|
1128
|
-
session = await sessionManager2.create({
|
|
1129
|
-
backendId: effectiveBackend,
|
|
1130
|
-
parentSessionId: envContext.parentSessionId ?? void 0,
|
|
1131
|
-
depth: envContext.depth + 1,
|
|
1132
|
-
metadata: validatedMetadata,
|
|
1133
|
-
expectedActiveCount: activeHealthyCount,
|
|
1134
|
-
expectedActiveStaleThresholdMs: staleThresholdMs
|
|
1135
|
-
});
|
|
1136
|
-
break;
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1139
|
-
lastCreateError = message;
|
|
1140
|
-
if (message.includes("RELAY_CONCURRENT_LIMIT_RACE") && attempt < maxCreateAttempts - 1) {
|
|
1141
|
-
continue;
|
|
1142
|
-
}
|
|
1143
|
-
if (message.includes("RELAY_CONCURRENT_LIMIT_RACE")) {
|
|
1144
|
-
return {
|
|
1145
|
-
sessionId: "",
|
|
1146
|
-
exitCode: 1,
|
|
1147
|
-
stdout: "",
|
|
1148
|
-
stderr: message,
|
|
1149
|
-
failureReason: "concurrent_limit_race"
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
return {
|
|
1153
|
-
sessionId: "",
|
|
1154
|
-
exitCode: 1,
|
|
1155
|
-
stdout: "",
|
|
1156
|
-
stderr: message,
|
|
1157
|
-
failureReason: "unknown"
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
if (!session) {
|
|
1097
|
+
let session;
|
|
1098
|
+
try {
|
|
1099
|
+
session = await sessionManager2.create({
|
|
1100
|
+
backendId: effectiveBackend,
|
|
1101
|
+
parentSessionId: envContext.parentSessionId ?? void 0,
|
|
1102
|
+
depth: envContext.depth + 1,
|
|
1103
|
+
metadata: validatedMetadata
|
|
1104
|
+
});
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1162
1107
|
return {
|
|
1163
1108
|
sessionId: "",
|
|
1164
1109
|
exitCode: 1,
|
|
1165
1110
|
stdout: "",
|
|
1166
|
-
stderr:
|
|
1167
|
-
failureReason: "
|
|
1111
|
+
stderr: message,
|
|
1112
|
+
failureReason: "unknown"
|
|
1168
1113
|
};
|
|
1169
1114
|
}
|
|
1170
1115
|
agentEventStore?.record({
|
|
@@ -1626,21 +1571,13 @@ ${input.prompt}`;
|
|
|
1626
1571
|
};
|
|
1627
1572
|
}
|
|
1628
1573
|
}
|
|
1629
|
-
var
|
|
1574
|
+
var spawnAgentInputSchema;
|
|
1630
1575
|
var init_spawn_agent = __esm({
|
|
1631
1576
|
"src/mcp-server/tools/spawn-agent.ts"() {
|
|
1632
1577
|
"use strict";
|
|
1633
1578
|
init_recursion_guard();
|
|
1634
1579
|
init_logger();
|
|
1635
|
-
|
|
1636
|
-
TASK_ID_PATTERN = /^(TASK|GOAL)-\d{3,}$/;
|
|
1637
|
-
MAX_METADATA_SIZE_BYTES = 8 * 1024;
|
|
1638
|
-
MAX_METADATA_KEY_COUNT = 20;
|
|
1639
|
-
MAX_METADATA_KEY_LENGTH = 64;
|
|
1640
|
-
MAX_METADATA_STRING_VALUE_LENGTH = 1024;
|
|
1641
|
-
MAX_METADATA_NESTING_DEPTH = 3;
|
|
1642
|
-
DEFAULT_MAX_ACTIVE_SESSIONS = 20;
|
|
1643
|
-
DEFAULT_STALE_THRESHOLD_MS = 3e5;
|
|
1580
|
+
init_metadata_validation();
|
|
1644
1581
|
spawnAgentInputSchema = z2.object({
|
|
1645
1582
|
fallbackBackend: z2.enum(["claude", "codex", "gemini"]).optional().describe(
|
|
1646
1583
|
"Optional fallback backend. Used only when BackendSelector is not active or cannot determine a backend. When BackendSelector is active, backend is auto-selected by priority: preferredBackend > agentType mapping > taskType mapping > default (claude)."
|
|
@@ -1780,7 +1717,31 @@ var init_conflict_detector = __esm({
|
|
|
1780
1717
|
});
|
|
1781
1718
|
|
|
1782
1719
|
// src/mcp-server/tools/spawn-agents-parallel.ts
|
|
1783
|
-
async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory"
|
|
1720
|
+
async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory") {
|
|
1721
|
+
for (let index = 0; index < agents.length; index += 1) {
|
|
1722
|
+
try {
|
|
1723
|
+
resolveValidatedSessionMetadata(agents[index]);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1726
|
+
const reason = `RELAY_METADATA_VALIDATION: agent index ${index}: ${message}`;
|
|
1727
|
+
return {
|
|
1728
|
+
results: agents.map((agent, resultIndex) => ({
|
|
1729
|
+
index: resultIndex,
|
|
1730
|
+
sessionId: "",
|
|
1731
|
+
exitCode: 1,
|
|
1732
|
+
stdout: "",
|
|
1733
|
+
stderr: reason,
|
|
1734
|
+
error: reason,
|
|
1735
|
+
failureReason: "metadata_validation",
|
|
1736
|
+
...agent.label ? { label: agent.label } : {},
|
|
1737
|
+
originalInput: agent
|
|
1738
|
+
})),
|
|
1739
|
+
totalCount: agents.length,
|
|
1740
|
+
successCount: 0,
|
|
1741
|
+
failureCount: agents.length
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1784
1745
|
const envContext = buildContextFromEnv();
|
|
1785
1746
|
if (envContext.depth >= guard.getConfig().maxDepth) {
|
|
1786
1747
|
const reason = `Max depth exceeded: ${envContext.depth} >= ${guard.getConfig().maxDepth}`;
|
|
@@ -1839,8 +1800,7 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
|
|
|
1839
1800
|
childHttpUrl,
|
|
1840
1801
|
void 0,
|
|
1841
1802
|
agentEventStore,
|
|
1842
|
-
hookMemoryDir
|
|
1843
|
-
sessionHealthConfig
|
|
1803
|
+
hookMemoryDir
|
|
1844
1804
|
).then((result) => {
|
|
1845
1805
|
completedCount++;
|
|
1846
1806
|
onProgress?.({
|
|
@@ -1911,21 +1871,22 @@ var init_spawn_agents_parallel = __esm({
|
|
|
1911
1871
|
// src/mcp-server/tools/list-sessions.ts
|
|
1912
1872
|
import { z as z3 } from "zod";
|
|
1913
1873
|
async function executeListSessions(input, sessionManager2, options) {
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1874
|
+
const validatedInput = listSessionsInputSchema.parse(input);
|
|
1875
|
+
const normalizedBackendId = validatedInput.backendId ?? validatedInput.backend;
|
|
1876
|
+
if (validatedInput.backendId && validatedInput.backend && validatedInput.backendId !== validatedInput.backend) {
|
|
1916
1877
|
logger.warn(
|
|
1917
|
-
`list_sessions: both backendId and backend were provided; backendId="${
|
|
1878
|
+
`list_sessions: both backendId and backend were provided; backendId="${validatedInput.backendId}" is used`
|
|
1918
1879
|
);
|
|
1919
1880
|
}
|
|
1920
1881
|
const staleThresholdMs = (options?.staleThresholdSec ?? 300) * 1e3;
|
|
1921
1882
|
const sessions = await sessionManager2.list({
|
|
1922
1883
|
backendId: normalizedBackendId,
|
|
1923
|
-
limit:
|
|
1924
|
-
status:
|
|
1925
|
-
taskId:
|
|
1926
|
-
label:
|
|
1927
|
-
tags:
|
|
1928
|
-
staleOnly:
|
|
1884
|
+
limit: validatedInput.limit,
|
|
1885
|
+
status: validatedInput.staleOnly ? "active" : validatedInput.status,
|
|
1886
|
+
taskId: validatedInput.taskId,
|
|
1887
|
+
label: validatedInput.label,
|
|
1888
|
+
tags: validatedInput.tags,
|
|
1889
|
+
staleOnly: validatedInput.staleOnly ?? false,
|
|
1929
1890
|
staleThresholdMs
|
|
1930
1891
|
});
|
|
1931
1892
|
return {
|
|
@@ -1946,12 +1907,13 @@ var init_list_sessions = __esm({
|
|
|
1946
1907
|
"src/mcp-server/tools/list-sessions.ts"() {
|
|
1947
1908
|
"use strict";
|
|
1948
1909
|
init_logger();
|
|
1910
|
+
init_metadata_validation();
|
|
1949
1911
|
listSessionsInputSchema = z3.object({
|
|
1950
1912
|
backendId: z3.enum(["claude", "codex", "gemini"]).optional(),
|
|
1951
1913
|
backend: z3.enum(["claude", "codex", "gemini"]).optional(),
|
|
1952
1914
|
limit: z3.number().int().min(1).max(100).optional().default(10),
|
|
1953
1915
|
status: z3.enum(["active", "completed", "error"]).optional(),
|
|
1954
|
-
taskId: z3.string().optional(),
|
|
1916
|
+
taskId: z3.string().regex(TASK_ID_PATTERN).optional(),
|
|
1955
1917
|
label: z3.string().optional(),
|
|
1956
1918
|
tags: z3.array(z3.string()).optional(),
|
|
1957
1919
|
staleOnly: z3.boolean().optional().default(false)
|
|
@@ -2351,6 +2313,7 @@ var init_server = __esm({
|
|
|
2351
2313
|
init_get_context_status();
|
|
2352
2314
|
init_list_available_backends();
|
|
2353
2315
|
init_backend_selector();
|
|
2316
|
+
init_metadata_validation();
|
|
2354
2317
|
init_logger();
|
|
2355
2318
|
init_types();
|
|
2356
2319
|
init_response_formatter();
|
|
@@ -2372,7 +2335,6 @@ var init_server = __esm({
|
|
|
2372
2335
|
this.guard = new RecursionGuard(guardConfig);
|
|
2373
2336
|
this.backendSelector = new BackendSelector();
|
|
2374
2337
|
this.staleThresholdSec = relayConfig?.sessionHealth?.staleThresholdSec ?? 300;
|
|
2375
|
-
this.maxActiveSessions = relayConfig?.sessionHealth?.maxActiveSessions ?? 20;
|
|
2376
2338
|
this.hookMemoryDir = relayConfig?.hooks?.memoryDir ?? "./memory";
|
|
2377
2339
|
this.agentEventStore = new AgentEventStore({
|
|
2378
2340
|
backend: relayConfig?.eventStore?.backend,
|
|
@@ -2400,7 +2362,7 @@ var init_server = __esm({
|
|
|
2400
2362
|
this.agentEventStore
|
|
2401
2363
|
);
|
|
2402
2364
|
this.server = new McpServer(
|
|
2403
|
-
{ name: "agentic-relay", version: "1.
|
|
2365
|
+
{ name: "agentic-relay", version: "1.3.0" },
|
|
2404
2366
|
createMcpServerOptions()
|
|
2405
2367
|
);
|
|
2406
2368
|
this.registerTools(this.server);
|
|
@@ -2412,7 +2374,6 @@ var init_server = __esm({
|
|
|
2412
2374
|
agentEventStore;
|
|
2413
2375
|
sessionHealthMonitor;
|
|
2414
2376
|
staleThresholdSec;
|
|
2415
|
-
maxActiveSessions;
|
|
2416
2377
|
hookMemoryDir;
|
|
2417
2378
|
_childHttpServer;
|
|
2418
2379
|
_childHttpUrl;
|
|
@@ -2442,11 +2403,7 @@ var init_server = __esm({
|
|
|
2442
2403
|
this._childHttpUrl,
|
|
2443
2404
|
void 0,
|
|
2444
2405
|
this.agentEventStore,
|
|
2445
|
-
this.hookMemoryDir
|
|
2446
|
-
{
|
|
2447
|
-
maxActiveSessions: this.maxActiveSessions,
|
|
2448
|
-
staleThresholdMs: this.staleThresholdSec * 1e3
|
|
2449
|
-
}
|
|
2406
|
+
this.hookMemoryDir
|
|
2450
2407
|
);
|
|
2451
2408
|
const controlOptions = {
|
|
2452
2409
|
inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
|
|
@@ -2501,11 +2458,7 @@ var init_server = __esm({
|
|
|
2501
2458
|
this._childHttpUrl,
|
|
2502
2459
|
void 0,
|
|
2503
2460
|
this.agentEventStore,
|
|
2504
|
-
this.hookMemoryDir
|
|
2505
|
-
{
|
|
2506
|
-
maxActiveSessions: this.maxActiveSessions,
|
|
2507
|
-
staleThresholdMs: this.staleThresholdSec * 1e3
|
|
2508
|
-
}
|
|
2461
|
+
this.hookMemoryDir
|
|
2509
2462
|
);
|
|
2510
2463
|
const controlOptions = {
|
|
2511
2464
|
inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
|
|
@@ -2626,11 +2579,7 @@ var init_server = __esm({
|
|
|
2626
2579
|
this._childHttpUrl,
|
|
2627
2580
|
void 0,
|
|
2628
2581
|
this.agentEventStore,
|
|
2629
|
-
this.hookMemoryDir
|
|
2630
|
-
{
|
|
2631
|
-
maxActiveSessions: this.maxActiveSessions,
|
|
2632
|
-
staleThresholdMs: this.staleThresholdSec * 1e3
|
|
2633
|
-
}
|
|
2582
|
+
this.hookMemoryDir
|
|
2634
2583
|
);
|
|
2635
2584
|
const controlOptions = {
|
|
2636
2585
|
inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
|
|
@@ -2658,7 +2607,7 @@ var init_server = __esm({
|
|
|
2658
2607
|
backend: z7.enum(["claude", "codex", "gemini"]).optional().describe("Filter sessions by backend type."),
|
|
2659
2608
|
limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of sessions to return. Default: 10."),
|
|
2660
2609
|
status: z7.enum(["active", "completed", "error"]).optional().describe("Filter sessions by status."),
|
|
2661
|
-
taskId: z7.string().optional().describe("Filter by metadata.taskId."),
|
|
2610
|
+
taskId: z7.string().regex(TASK_ID_PATTERN).optional().describe("Filter by metadata.taskId."),
|
|
2662
2611
|
label: z7.string().optional().describe("Case-insensitive partial match for metadata.label."),
|
|
2663
2612
|
tags: z7.array(z7.string()).optional().describe("Filter sessions containing all provided tags."),
|
|
2664
2613
|
staleOnly: z7.boolean().optional().describe("When true, return only stale active sessions.")
|
|
@@ -2881,7 +2830,7 @@ var init_server = __esm({
|
|
|
2881
2830
|
sessionIdGenerator: () => randomUUID()
|
|
2882
2831
|
});
|
|
2883
2832
|
const server = new McpServer(
|
|
2884
|
-
{ name: "agentic-relay", version: "1.
|
|
2833
|
+
{ name: "agentic-relay", version: "1.3.0" },
|
|
2885
2834
|
createMcpServerOptions()
|
|
2886
2835
|
);
|
|
2887
2836
|
this.registerTools(server);
|
|
@@ -4291,7 +4240,8 @@ ${prompt}`;
|
|
|
4291
4240
|
|
|
4292
4241
|
// src/core/session-manager.ts
|
|
4293
4242
|
init_logger();
|
|
4294
|
-
|
|
4243
|
+
init_metadata_validation();
|
|
4244
|
+
import { readFile as readFile4, writeFile as writeFile4, readdir, mkdir as mkdir4, chmod, rename, unlink } from "fs/promises";
|
|
4295
4245
|
import { join as join4 } from "path";
|
|
4296
4246
|
import { homedir as homedir4 } from "os";
|
|
4297
4247
|
import { nanoid } from "nanoid";
|
|
@@ -4328,9 +4278,6 @@ function fromSessionData(data) {
|
|
|
4328
4278
|
var SessionManager = class _SessionManager {
|
|
4329
4279
|
static SESSION_ID_PATTERN = /^relay-[A-Za-z0-9_-]+$/;
|
|
4330
4280
|
static DEFAULT_STALE_THRESHOLD_MS = 3e5;
|
|
4331
|
-
static CREATE_LOCK_FILE = ".create.lock";
|
|
4332
|
-
static CREATE_LOCK_RETRY_DELAY_MS = 10;
|
|
4333
|
-
static CREATE_LOCK_MAX_RETRIES = 100;
|
|
4334
4281
|
static PROTECTED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
4335
4282
|
"taskId",
|
|
4336
4283
|
"parentTaskId",
|
|
@@ -4390,35 +4337,6 @@ var SessionManager = class _SessionManager {
|
|
|
4390
4337
|
if (session.status !== "active") return false;
|
|
4391
4338
|
return this.getLastActivityAtMs(session) + staleThresholdMs < now;
|
|
4392
4339
|
}
|
|
4393
|
-
async countHealthyActiveSessions(staleThresholdMs) {
|
|
4394
|
-
const activeSessions = await this.list({ status: "active" });
|
|
4395
|
-
const now = Date.now();
|
|
4396
|
-
return activeSessions.filter(
|
|
4397
|
-
(session) => !this.isStale(session, staleThresholdMs, now)
|
|
4398
|
-
).length;
|
|
4399
|
-
}
|
|
4400
|
-
async acquireCreateLock() {
|
|
4401
|
-
await this.ensureDir();
|
|
4402
|
-
const lockPath = join4(this.sessionsDir, _SessionManager.CREATE_LOCK_FILE);
|
|
4403
|
-
for (let attempt = 0; attempt < _SessionManager.CREATE_LOCK_MAX_RETRIES; attempt += 1) {
|
|
4404
|
-
try {
|
|
4405
|
-
const handle = await open(lockPath, "wx", 384);
|
|
4406
|
-
return async () => {
|
|
4407
|
-
await handle.close().catch(() => void 0);
|
|
4408
|
-
await unlink(lockPath).catch(() => void 0);
|
|
4409
|
-
};
|
|
4410
|
-
} catch (error) {
|
|
4411
|
-
const code = error.code;
|
|
4412
|
-
if (code !== "EEXIST") {
|
|
4413
|
-
throw error;
|
|
4414
|
-
}
|
|
4415
|
-
}
|
|
4416
|
-
await new Promise(
|
|
4417
|
-
(resolve3) => setTimeout(resolve3, _SessionManager.CREATE_LOCK_RETRY_DELAY_MS)
|
|
4418
|
-
);
|
|
4419
|
-
}
|
|
4420
|
-
throw new Error("RELAY_CONCURRENT_LIMIT_RACE: failed to acquire create lock");
|
|
4421
|
-
}
|
|
4422
4340
|
mergeMetadata(existing, updates) {
|
|
4423
4341
|
const merged = { ...existing };
|
|
4424
4342
|
for (const [key, value] of Object.entries(updates)) {
|
|
@@ -4432,36 +4350,23 @@ var SessionManager = class _SessionManager {
|
|
|
4432
4350
|
}
|
|
4433
4351
|
/** Create a new relay session. */
|
|
4434
4352
|
async create(params) {
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
depth: params.depth ?? 0,
|
|
4453
|
-
createdAt: now,
|
|
4454
|
-
updatedAt: now,
|
|
4455
|
-
status: "active",
|
|
4456
|
-
lastHeartbeatAt: now,
|
|
4457
|
-
staleNotifiedAt: null,
|
|
4458
|
-
metadata: params.metadata ?? {}
|
|
4459
|
-
};
|
|
4460
|
-
await this.writeSession(session);
|
|
4461
|
-
return session;
|
|
4462
|
-
} finally {
|
|
4463
|
-
await releaseCreateLock();
|
|
4464
|
-
}
|
|
4353
|
+
await this.ensureDir();
|
|
4354
|
+
const now = /* @__PURE__ */ new Date();
|
|
4355
|
+
const session = {
|
|
4356
|
+
relaySessionId: `relay-${nanoid()}`,
|
|
4357
|
+
nativeSessionId: params.nativeSessionId ?? null,
|
|
4358
|
+
backendId: params.backendId,
|
|
4359
|
+
parentSessionId: params.parentSessionId ?? null,
|
|
4360
|
+
depth: params.depth ?? 0,
|
|
4361
|
+
createdAt: now,
|
|
4362
|
+
updatedAt: now,
|
|
4363
|
+
status: "active",
|
|
4364
|
+
lastHeartbeatAt: now,
|
|
4365
|
+
staleNotifiedAt: null,
|
|
4366
|
+
metadata: validateMetadata(params.metadata)
|
|
4367
|
+
};
|
|
4368
|
+
await this.writeSession(session);
|
|
4369
|
+
return session;
|
|
4465
4370
|
}
|
|
4466
4371
|
/** Update an existing session. */
|
|
4467
4372
|
async update(relaySessionId, updates) {
|
|
@@ -4475,7 +4380,9 @@ var SessionManager = class _SessionManager {
|
|
|
4475
4380
|
const updated = {
|
|
4476
4381
|
...session,
|
|
4477
4382
|
...updates,
|
|
4478
|
-
metadata:
|
|
4383
|
+
metadata: validateMetadata(
|
|
4384
|
+
updates.metadata ? this.mergeMetadata(session.metadata, updates.metadata) : session.metadata
|
|
4385
|
+
),
|
|
4479
4386
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4480
4387
|
};
|
|
4481
4388
|
await this.writeSession(updated);
|
|
@@ -4720,7 +4627,7 @@ function deepMerge(target, source) {
|
|
|
4720
4627
|
for (const key of Object.keys(source)) {
|
|
4721
4628
|
const sourceVal = source[key];
|
|
4722
4629
|
const targetVal = result[key];
|
|
4723
|
-
if (
|
|
4630
|
+
if (isPlainObject2(sourceVal) && isPlainObject2(targetVal)) {
|
|
4724
4631
|
result[key] = deepMerge(
|
|
4725
4632
|
targetVal,
|
|
4726
4633
|
sourceVal
|
|
@@ -4731,14 +4638,14 @@ function deepMerge(target, source) {
|
|
|
4731
4638
|
}
|
|
4732
4639
|
return result;
|
|
4733
4640
|
}
|
|
4734
|
-
function
|
|
4641
|
+
function isPlainObject2(value) {
|
|
4735
4642
|
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
4736
4643
|
}
|
|
4737
4644
|
function getByPath(obj, path2) {
|
|
4738
4645
|
const parts = path2.split(".");
|
|
4739
4646
|
let current = obj;
|
|
4740
4647
|
for (const part of parts) {
|
|
4741
|
-
if (!
|
|
4648
|
+
if (!isPlainObject2(current)) return void 0;
|
|
4742
4649
|
current = current[part];
|
|
4743
4650
|
}
|
|
4744
4651
|
return current;
|
|
@@ -4748,7 +4655,7 @@ function setByPath(obj, path2, value) {
|
|
|
4748
4655
|
let current = obj;
|
|
4749
4656
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
4750
4657
|
const part = parts[i];
|
|
4751
|
-
if (!
|
|
4658
|
+
if (!isPlainObject2(current[part])) {
|
|
4752
4659
|
current[part] = {};
|
|
4753
4660
|
}
|
|
4754
4661
|
current = current[part];
|
|
@@ -4857,7 +4764,7 @@ var ConfigManager = class {
|
|
|
4857
4764
|
try {
|
|
4858
4765
|
const raw = await readFile5(filePath, "utf-8");
|
|
4859
4766
|
const parsed = JSON.parse(raw);
|
|
4860
|
-
if (!
|
|
4767
|
+
if (!isPlainObject2(parsed)) return {};
|
|
4861
4768
|
return parsed;
|
|
4862
4769
|
} catch {
|
|
4863
4770
|
return {};
|
|
@@ -6305,7 +6212,7 @@ function createVersionCommand(registry2) {
|
|
|
6305
6212
|
description: "Show relay and backend versions"
|
|
6306
6213
|
},
|
|
6307
6214
|
async run() {
|
|
6308
|
-
const relayVersion = "1.
|
|
6215
|
+
const relayVersion = "1.3.0";
|
|
6309
6216
|
console.log(`agentic-relay v${relayVersion}`);
|
|
6310
6217
|
console.log("");
|
|
6311
6218
|
console.log("Backends:");
|
|
@@ -6658,7 +6565,7 @@ void configManager.getConfig().then((config) => {
|
|
|
6658
6565
|
var main = defineCommand10({
|
|
6659
6566
|
meta: {
|
|
6660
6567
|
name: "relay",
|
|
6661
|
-
version: "1.
|
|
6568
|
+
version: "1.3.0",
|
|
6662
6569
|
description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
|
|
6663
6570
|
},
|
|
6664
6571
|
subCommands: {
|
package/package.json
CHANGED